TwoSameInput



<!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>ツーサム v9 (修正完全版)</title>
<style>
    :root {
        --bg-color: #121212;
        --text-color: #eee;
        --accent: #00b4d8;
        --key-bg: #1f1f1f;
        --tab-active: #2a9d8f;
        --del-color: #e63946;
        --wait-color: #fca311; /* 1回タップ時の色(黄色) */
    }

    body {
        margin: 0; padding: 0;
        background-color: var(--bg-color);
        color: var(--text-color);
        font-family: "Hiragino Kaku Gothic ProN", sans-serif;
        height: 100vh;
        width: 100vw;
        display: flex;
        flex-direction: column;
        overflow: hidden;
        user-select: none;
        touch-action: pan-y;
    }

    /* --- 上部エリア --- */
    #top-area {
        height: 14vh;
        background: #000;
        border-bottom: 1px solid #444;
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        padding: 5px 10px;
        box-sizing: border-box;
    }

    #display-wrapper {
        display: flex;
        align-items: flex-start;
        flex: 1;
        overflow: hidden;
    }

    #input-line {
        font-size: 20px;
        line-height: 1.3;
        white-space: pre-wrap;
        overflow-y: auto;
        flex: 1;
        margin-right: 10px;
        height: 100%;
        border: 1px solid #333;
        padding: 5px;
        border-radius: 4px;
        word-break: break-all;
    }

    /* 未確定文字(青下線) */
    .composing { 
        background: #003566; 
        border-bottom: 2px solid #00b4d8; 
    }

    /* ツールボタン */
    .tool-btn-group {
        display: flex;
        flex-direction: column;
        gap: 5px;
        justify-content: flex-start;
    }
    .tool-btn {
        border: none;
        border-radius: 4px;
        width: 60px;
        height: 35px;
        font-weight: bold;
        font-size: 13px;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    #btn-copy { background: #2a9d8f; color: #fff; }
    #btn-del { background: var(--del-color); color: #fff; }
    .tool-btn:active { transform: scale(0.95); opacity: 0.8; }

    /* 候補バー */
    #candidate-bar {
        height: 35px;
        display: flex;
        overflow-x: auto;
        align-items: center;
        border-top: 1px solid #333;
        white-space: nowrap;
        background: #1a1a1a;
        padding-left: 5px;
    }
    .cand-chip {
        background: #333; padding: 4px 12px; margin-right: 6px;
        border-radius: 12px; font-size: 14px; border: 1px solid #555;
    }
    .cand-chip:active { background: var(--accent); color: #000; }

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

    /* モードA: 日本語ボード */
    #twothumb-board {
        display: grid;
        grid-template-columns: 1.4fr 1fr;
        gap: 2px;
        padding: 2px;
        height: 100%;
        width: 100%;
        box-sizing: border-box;
    }
    .panel-grid { display: grid; gap: 2px; }
    #left-panel { grid-template-columns: 1fr 1fr; grid-template-rows: repeat(6, 1fr); }
    #right-panel { grid-template-columns: 1fr; grid-template-rows: repeat(5, 1fr); }

    .key {
        display: flex; align-items: center; justify-content: center;
        font-weight: bold; background: var(--key-bg);
        border: 1px solid #333; border-radius: 4px;
        font-size: 18px;
        cursor: pointer;
    }
    .key:active { background: #444; }
    .key-left.active { background: var(--accent); color: #000; font-weight: 900; border: 2px solid #fff; }
    .key-right { font-size: 26px; background: #222; }
    
    /* モードB: スクロールグリッド */
    #scroll-grid {
        display: none;
        grid-template-columns: repeat(auto-fill, minmax(65px, 1fr));
        grid-auto-rows: 65px;
        gap: 6px;
        padding: 10px;
        overflow-y: auto;
        height: 100%;
        align-content: start;
        -webkit-overflow-scrolling: touch;
        box-sizing: border-box;
        padding-bottom: 80px;
    }
    .scroll-key {
        background: #222; border: 1px solid #444; border-radius: 8px;
        display: flex; align-items: center; justify-content: center;
        font-size: 24px; color: #fff;
        position: relative;
        cursor: pointer;
        transition: background-color 0.1s;
    }
    /* 1回タップした時の色(黄色:待機中) */
    .scroll-key.waiting { background: var(--wait-color); color: #000; border-color: var(--wait-color); }
    /* 確定した時の色(青) */
    .scroll-key.tapped { background: #2a9d8f; color: #fff; transform: scale(0.95); }
    
    .key-special { font-size: 14px; color: #aaa; border: 1px dashed #666; flex-direction: column; }
    .key-enter { background: #223344; color: #aab; }

    /* --- 下部タブ --- */
    #tab-bar {
        height: 8vh;
        background: #000;
        border-top: 1px solid #444;
        display: flex;
    }
    .tab {
        flex: 1;
        display: flex; align-items: center; justify-content: center;
        font-size: 12px; font-weight: bold; color: #666;
        border-right: 1px solid #222;
        flex-direction: column;
        cursor: pointer;
    }
    .tab span { font-size: 18px; margin-bottom: 2px; }
    .tab.active { background: #1a1a1a; color: var(--tab-active); border-top: 3px solid var(--tab-active); }

</style>
</head>
<body>

<div id="top-area">
    <div id="display-wrapper">
        <div id="input-line">
            <span id="committed-text"></span><span id="composing-text" class="composing"></span>
        </div>
        <div class="tool-btn-group">
            <button id="btn-del" class="tool-btn" onclick="deleteChar()">削除</button>
            <button id="btn-copy" class="tool-btn" onclick="copyAll()">コピー</button>
        </div>
    </div>
    <div id="candidate-bar"><span style="color:#666;font-size:12px;">変換候補なし</span></div>
</div>

<div id="main-stage">
    <div id="twothumb-board">
        <div id="left-panel" class="panel-grid">
            <div class="key key-left" onclick="setRow('a', this)">あ</div>
            <div class="key key-left" onclick="setRow('k', this)">か</div>
            <div class="key key-left" onclick="setRow('s', this)">さ</div>
            <div class="key key-left" onclick="setRow('t', this)">た</div>
            <div class="key key-left" onclick="setRow('n', this)">な</div>
            <div class="key key-left" onclick="setRow('h', this)">は</div>
            <div class="key key-left" onclick="setRow('m', this)">ま</div>
            <div class="key key-left" onclick="setRow('y', this)">や</div>
            <div class="key key-left" onclick="setRow('r', this)">ら</div>
            <div class="key key-left" onclick="setRow('w', this)">わ</div>
            
            <div class="key" style="background:#4a4e69; grid-column: span 2;" onclick="modifyChar()">゛゜小文字</div>
        </div>
        <div id="right-panel" class="panel-grid">
            <div class="key key-right" id="r0" onclick="inputJP(0)"></div>
            <div class="key key-right" id="r1" onclick="inputJP(1)"></div>
            <div class="key key-right" id="r2" onclick="inputJP(2)"></div>
            <div class="key key-right" id="r3" onclick="inputJP(3)"></div>
            <div class="key key-right" id="r4" onclick="inputJP(4)"></div>
        </div>
    </div>

    <div id="scroll-grid"></div>
</div>

<div id="tab-bar">
    <div class="tab active" onclick="switchMode('hira', this)"><span>あ</span>平仮名</div>
    <div class="tab" onclick="switchMode('kata', this)"><span>ア</span>カタカナ</div>
    <div class="tab" onclick="switchMode('alpha', this)"><span>Aa</span>英字</div>
    <div class="tab" onclick="switchMode('num', this)"><span>12</span>数字</div>
    <div class="tab" onclick="switchMode('sym', this)"><span>#%</span>記号</div>
    <div class="tab" onclick="switchMode('emoji', this)"><span>😀</span>絵文字</div>
</div>

<script>
    // --- データ ---
    const jpMap = {
        'hira': {
            'a':['あ','い','う','え','お'], 'k':['か','き','く','け','こ'],
            's':['さ','し','す','せ','そ'], 't':['た','ち','つ','て','と'],
            'n':['な','に','ぬ','ね','の'], 'h':['は','ひ','ふ','へ','ほ'],
            'm':['ま','み','む','め','も'], 'y':['や','(','ゆ',')','よ'],
            'r':['ら','り','る','れ','ろ'], 'w':['わ','を','ん','ー','、']
        },
        'kata': {
            'a':['ア','イ','ウ','エ','オ'], 'k':['カ','キ','ク','ケ','コ'],
            's':['サ','シ','ス','セ','ソ'], 't':['タ','チ','ツ','テ','ト'],
            'n':['ナ','ニ','ヌ','ネ','ノ'], 'h':['ハ','ヒ','フ','ヘ','ホ'],
            'm':['マ','ミ','ム','メ','モ'], 'y':['ヤ','(','ユ',')','ヨ'],
            'r':['ラ','リ','ル','レ','ロ'], 'w':['ワ','ヲ','ン','ー','、']
        }
    };

    const dakutenMap = {
        'か':'が','き':'ぎ','く':'ぐ','け':'げ','こ':'ご',
        'さ':'ざ','し':'じ','す':'ず','せ':'ぜ','そ':'ぞ',
        'た':'だ','ち':'ぢ','つ':'づ','て':'で','と':'ど',
        'は':'ば','ひ':'び','ふ':'ぶ','へ':'べ','ほ':'ぼ',
        'ば':'ぱ','び':'ぴ','ぶ':'ぷ','べ':'ぺ','ぼ':'ぽ',
        'ぱ':'は','ぴ':'ひ','ぷ':'ふ','ぺ':'へ','ぽ':'ほ',
        'つ':'っ','や':'ゃ','ゆ':'ゅ','よ':'ょ',
        'あ':'ぁ','い':'ぃ','う':'ぅ','え':'ぇ','お':'ぉ',
        'カ':'ガ','サ':'ザ','タ':'ダ','ハ':'バ','バ':'パ','パ':'ハ',
        'ツ':'ッ','ヤ':'ャ','ユ':'ュ','ヨ':'ョ','ア':'ァ'
    };

    const listData = {
        'alpha': ["ENTER", "SPACE"].concat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.,!?@_".split("")),
        'num':   ["ENTER", "SPACE"].concat("1234567890+-*/=.,:;()¥%".split("")),
        'sym':   ["ENTER", "SPACE"].concat("!?”’()[]{}<>〜=+−/*@#$%&¥|_…↑↓←→★☆♪◯●◎".split("")),
        'emoji': ["ENTER", "SPACE"].concat("😀😂😊😍😢😡👍👎🙏❤️🔥✨🎉🍺🍜🚗🏠💡🐶🐱".split(""))
    };

    // --- 状態 ---
    let currentMode = 'hira';
    let currentRow = 'a';
    let committedStr = ""; 
    let composingStr = "";
    
    // ダブルタップ制御用
    let lastTapTarget = null;
    let tapTimeout = null;

    // --- DOM ---
    const elCommitted = document.getElementById('committed-text');
    const elComposing = document.getElementById('composing-text');
    const elTwothumb = document.getElementById('twothumb-board');
    const elScroll = document.getElementById('scroll-grid');
    const rightKeys = Array.from(document.querySelectorAll('.key-right'));
    const inputLine = document.getElementById('input-line');

    // 初期化
    setRow('a', document.querySelector('.key-left'));

    // --- モード切替 ---
    function switchMode(mode, tabEl) {
        currentMode = mode;
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        tabEl.classList.add('active');

        if (mode === 'hira' || mode === 'kata') {
            elTwothumb.style.display = 'grid';
            elScroll.style.display = 'none';
            setRow(currentRow, null);
        } else {
            elTwothumb.style.display = 'none';
            elScroll.style.display = 'grid';
            renderScrollGrid(mode);
        }
    }

    // --- 日本語入力 ---
    function setRow(rowKey, element) {
        currentRow = rowKey;
        if(element) {
            document.querySelectorAll('.key-left').forEach(k => k.classList.remove('active'));
            element.classList.add('active');
        }
        const map = (currentMode === 'kata') ? jpMap['kata'] : jpMap['hira'];
        const chars = map[rowKey];
        rightKeys.forEach((k, i) => k.innerText = chars[i]);
    }

    function inputJP(index) {
        const map = (currentMode === 'kata') ? jpMap['kata'] : jpMap['hira'];
        const char = map[currentRow][index];
        composingStr += char;
        updateDisplay();
        updateCandidates();
    }

    function modifyChar() {
        if (!composingStr) return;
        let lastChar = composingStr.slice(-1);
        let prefix = composingStr.slice(0, -1);
        if (dakutenMap[lastChar]) {
            composingStr = prefix + dakutenMap[lastChar];
            updateDisplay();
            updateCandidates();
        }
    }

    // --- スクロールグリッド (修正版: Mouse/Touch両対応) ---
    function renderScrollGrid(mode) {
        elScroll.innerHTML = "";
        const chars = listData[mode];
        chars.forEach(item => {
            const btn = document.createElement('div');
            btn.className = 'scroll-key';
            
            let displayLabel = item;
            let actualInput = item;

            if(item === "SPACE") {
                displayLabel = "空白";
                actualInput = " ";
                btn.classList.add('key-special');
            } else if (item === "ENTER") {
                displayLabel = "改行";
                actualInput = "\n";
                btn.classList.add('key-special', 'key-enter');
            }

            btn.innerText = displayLabel;
            
            // イベントリスナー (PointerEventを使用し、スマホ/PC両対応にする)
            // pointerdownだとスクロールの誤爆があるため、clickで判定する
            btn.addEventListener('click', (e) => {
                handleDoubleTapLogic(btn, actualInput);
            });

            elScroll.appendChild(btn);
        });
    }

    // --- ダブルタップ判定ロジック (コア部分) ---
    function handleDoubleTapLogic(btnElement, char) {
        // 違うボタンを押した、または初回クリック
        if (lastTapTarget !== btnElement) {
            // 前のボタンの待機状態を解除
            if (lastTapTarget) {
                lastTapTarget.classList.remove('waiting');
            }
            
            // 今回のボタンを待機状態(黄色)にする
            lastTapTarget = btnElement;
            btnElement.classList.add('waiting');
            
            // タイムアウト設定 (1.5秒待つ)
            if (tapTimeout) clearTimeout(tapTimeout);
            tapTimeout = setTimeout(() => {
                // 時間切れリセット
                if(lastTapTarget === btnElement) {
                    btnElement.classList.remove('waiting');
                    lastTapTarget = null;
                }
            }, 1500); // ゆっくりでも押せるように長めに設定
            
        } else {
            // 2回目クリック成功! (同じボタン)
            clearTimeout(tapTimeout);
            
            // 入力実行
            inputDirect(char);
            
            // アニメーション
            btnElement.classList.remove('waiting');
            btnElement.classList.add('tapped');
            setTimeout(() => btnElement.classList.remove('tapped'), 150);
            
            lastTapTarget = null;
        }
    }

    // --- 割り込み確定入力 ---
    function inputDirect(char) {
        // 1. 未確定があれば強制確定
        if (composingStr.length > 0) {
            committedStr += composingStr;
            composingStr = "";
        }
        
        // 2. 文字追加
        committedStr += char;
        
        // 3. 画面更新
        updateDisplay();
        updateCandidates();
    }

    // --- 削除 ---
    function deleteChar() {
        if (composingStr.length > 0) {
            composingStr = composingStr.slice(0, -1);
        } else if (committedStr.length > 0) {
            committedStr = committedStr.slice(0, -1);
        }
        updateDisplay();
        updateCandidates();
    }

    // --- 表示更新 ---
    function updateDisplay() {
        elCommitted.innerText = committedStr;
        elComposing.innerText = composingStr;
        
        // 自動スクロール
        setTimeout(() => { 
            inputLine.scrollTop = inputLine.scrollHeight; 
        }, 0);
    }

    // --- コピー ---
    function copyAll() {
        const text = committedStr + composingStr;
        if(!text) return;
        navigator.clipboard.writeText(text).then(() => {
            const btn = document.getElementById('btn-copy');
            const orig = btn.innerText;
            btn.innerText = "OK!";
            setTimeout(() => btn.innerText = orig, 1000);
        }).catch(e => alert("コピー失敗"));
    }

    // --- 候補表示 ---
    function updateCandidates() {
        const bar = document.getElementById('candidate-bar');
        bar.innerHTML = "";
        
        if(!composingStr) {
            bar.innerHTML = '<span style="color:#666;font-size:12px;padding:0 5px;">変換候補なし</span>';
            return;
        }

        const list = [composingStr];
        if(currentMode === 'hira') list.push(hiraToKata(composingStr));
        
        list.forEach(c => {
            const chip = document.createElement('div');
            chip.className = 'cand-chip';
            chip.innerText = c;
            // clickで確実に反応させる
            chip.onclick = () => commitCandidate(c);
            bar.appendChild(chip);
        });
    }
    
    function commitCandidate(text) {
        committedStr += text;
        composingStr = "";
        updateDisplay();
        updateCandidates();
    }

    function hiraToKata(str) {
        return str.replace(/[\u3041-\u3096]/g, ch => String.fromCharCode(ch.charCodeAt(0) + 0x60));
    }
</script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
TwoSameInput|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word 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