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>ツーサム v11 (見切れ修正&カラフル版)</title>
<style>
    :root {
        --bg-color: #121212;
        --text-color: #fff;
        
        /* --- 行ごとの色定義 (マテリアルカラー風) --- */
        --c-a: #ef476f; /* あ: ピンク赤 */
        --c-k: #f78c6b; /* か: オレンジ */
        --c-s: #ffd166; /* さ: 黄色 */
        --c-t: #06d6a0; /* た: エメラルド */
        --c-n: #118ab2; /* な: 青 */
        --c-h: #073b4c; /* は: 紺 */
        --c-m: #9d4edd; /* ま: 紫 */
        --c-y: #ff006e; /* や: マゼンタ */
        --c-r: #8338ec; /* ら: すみれ */
        --c-w: #3a86ff; /* わ: スカイブルー */
        
        /* --- タブごとの色 --- */
        --tab-hira: #ef476f;
        --tab-kata: #f78c6b;
        --tab-alpha: #118ab2;
        --tab-num: #06d6a0;
        --tab-sym: #9d4edd;
        --tab-emoji: #ffd166;
    }

    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 {
        /* スマホのセーフエリア(ノッチ等) + 20px の余白を確保 */
        padding-top: max(env(safe-area-inset-top), 20px);
        padding-bottom: 5px;
        padding-left: 8px;
        padding-right: 8px;
        
        height: 18%; /* 少し広めに確保 */
        min-height: 100px;
        background: #000;
        border-bottom: 2px solid #333;
        
        display: flex;
        flex-direction: column;
        justify-content: flex-end; /* 下揃えにして余白を上に作る */
        flex-shrink: 0;
        z-index: 10;
        box-sizing: border-box;
    }

    #display-wrapper {
        display: flex;
        align-items: stretch;
        flex: 1;
        overflow: hidden;
        margin-bottom: 6px;
    }

    #input-line {
        font-size: 20px;
        line-height: 1.4;
        white-space: pre-wrap;
        overflow-y: auto;
        flex: 1;
        margin-right: 8px;
        border: 1px solid #555;
        background: #1a1a1a;
        padding: 8px;
        border-radius: 8px;
        word-break: break-all;
    }

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

    /* 右側のツールボタン群 */
    .tool-btn-group {
        display: flex;
        flex-direction: column;
        gap: 5px;
        width: 60px;
    }
    .tool-btn {
        border: none;
        border-radius: 6px;
        flex: 1;
        font-weight: bold;
        font-size: 13px;
        cursor: pointer;
        color: #fff;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 2px 0 rgba(0,0,0,0.3);
    }
    #btn-copy { background: #2a9d8f; }
    #btn-del { background: #d62828; }
    .tool-btn:active { transform: translateY(2px); box-shadow: none; opacity: 0.8; }

    /* 候補バー */
    #candidate-bar {
        height: 36px;
        display: flex;
        overflow-x: auto;
        align-items: center;
        border-top: 1px solid #333;
        white-space: nowrap;
        background: #222;
        padding-left: 5px;
        border-radius: 4px;
        flex-shrink: 0;
    }
    .cand-chip {
        background: #444; padding: 4px 12px; margin-right: 8px;
        border-radius: 12px; font-size: 14px; border: 1px solid #666;
    }
    .cand-chip:active { background: #fff; color: #000; }

    /* --- メインエリア (残りのスペース) --- */
    #main-stage {
        flex: 1;
        position: relative;
        overflow: hidden;
        background: #111;
        padding: 4px;
    }

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

    .key {
        display: flex; align-items: center; justify-content: center;
        font-weight: bold; 
        border-radius: 8px;
        font-size: 18px;
        cursor: pointer;
        color: #fff;
        text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
        box-shadow: 0 3px 0 rgba(0,0,0,0.3);
        transition: transform 0.05s, background-color 0.2s;
    }
    .key:active { transform: translateY(2px); box-shadow: none; }

    /* --- 左キーの色分け --- */
    .row-a { background: var(--c-a); }
    .row-k { background: var(--c-k); }
    .row-s { background: var(--c-s); color:#222; text-shadow:none; } /* 黄色は文字黒 */
    .row-t { background: var(--c-t); }
    .row-n { background: var(--c-n); }
    .row-h { background: var(--c-h); }
    .row-m { background: var(--c-m); }
    .row-y { background: var(--c-y); }
    .row-r { background: var(--c-r); }
    .row-w { background: var(--c-w); }
    
    .key-func { background: #444; font-size: 14px; }
    
    /* 選択中の左キー強調 */
    .key-left.active { 
        border: 3px solid #fff; 
        transform: scale(0.96);
        filter: brightness(1.1);
        z-index: 2;
    }

    /* 右キー(初期状態は暗め、JSで色が変わる) */
    .key-right { font-size: 28px; background: #333; }
    
    /* モードB: スクロールグリッド */
    #scroll-grid {
        display: none;
        grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
        grid-auto-rows: 60px;
        gap: 6px;
        padding: 5px;
        overflow-y: auto;
        height: 100%;
        align-content: start;
        box-sizing: border-box;
        padding-bottom: 60px;
    }
    .scroll-key {
        background: #333; border: 1px solid #555; border-radius: 8px;
        display: flex; align-items: center; justify-content: center;
        font-size: 22px; color: #fff;
        cursor: pointer;
    }
    .scroll-key.waiting { background: #ffd166; color: #000; border: 2px solid #fff; }
    .scroll-key.tapped { background: #06d6a0; transform: scale(0.95); }
    .key-special { font-size: 14px; color: #aaa; border: 1px dashed #666; flex-direction: column; }
    .key-enter { background: #264653; }

    /* --- 下部タブ (固定) --- */
    #tab-bar {
        height: 60px;
        background: #000;
        border-top: 1px solid #333;
        display: flex;
        flex-shrink: 0;
        z-index: 10;
        padding-bottom: max(env(safe-area-inset-bottom), 0px); /* iPhone下部バー対応 */
    }
    .tab {
        flex: 1;
        display: flex; align-items: center; justify-content: center;
        font-size: 11px; font-weight: bold; color: #888;
        border-right: 1px solid #111;
        flex-direction: column;
        cursor: pointer;
        border-bottom: 4px solid transparent;
        transition: all 0.2s;
        background: #1a1a1a;
    }
    .tab span { font-size: 16px; margin-bottom: 3px; }
    
    /* タブのアクティブ色 */
    .t-hira.active { background: #2b0a10; color: var(--tab-hira); border-bottom-color: var(--tab-hira); }
    .t-kata.active { background: #2b1600; color: var(--tab-kata); border-bottom-color: var(--tab-kata); }
    .t-alpha.active { background: #00121a; color: var(--tab-alpha); border-bottom-color: var(--tab-alpha); }
    .t-num.active { background: #001a14; color: var(--tab-num); border-bottom-color: var(--tab-num); }
    .t-sym.active { background: #1a002b; color: var(--tab-sym); border-bottom-color: var(--tab-sym); }
    .t-emoji.active { background: #2b2200; color: var(--tab-emoji); border-bottom-color: var(--tab-emoji); }

</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">
            <div class="key key-left row-a" onclick="setRow('a', this)">あ</div>
            <div class="key key-left row-k" onclick="setRow('k', this)">か</div>
            <div class="key key-left row-s" onclick="setRow('s', this)">さ</div>
            <div class="key key-left row-t" onclick="setRow('t', this)">た</div>
            <div class="key key-left row-n" onclick="setRow('n', this)">な</div>
            <div class="key key-left row-h" onclick="setRow('h', this)">は</div>
            <div class="key key-left row-m" onclick="setRow('m', this)">ま</div>
            <div class="key key-left row-y" onclick="setRow('y', this)">や</div>
            <div class="key key-left row-r" onclick="setRow('r', this)">ら</div>
            <div class="key key-left row-w" onclick="setRow('w', this)">わ</div>
            
            <div class="key key-func" style="grid-column: span 2;" onclick="modifyChar()">゛゜小</div>
        </div>
        <div id="right-panel">
            <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 t-hira active" onclick="switchMode('hira', this)"><span>あ</span>平仮名</div>
    <div class="tab t-kata" onclick="switchMode('kata', this)"><span>ア</span>カタカナ</div>
    <div class="tab t-alpha" onclick="switchMode('alpha', this)"><span>Aa</span>英字</div>
    <div class="tab t-num" onclick="switchMode('num', this)"><span>12</span>数字</div>
    <div class="tab t-sym" onclick="switchMode('sym', this)"><span>#%</span>記号</div>
    <div class="tab t-emoji" 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 computedStyle = window.getComputedStyle(element);
            const bgColor = computedStyle.backgroundColor;
            const color = computedStyle.color;
            
            rightKeys.forEach(k => {
                k.style.borderColor = bgColor; 
                k.style.color = "#fff";
                k.style.backgroundColor = bgColor; 
                // 少し暗くして右側っぽさを出す、文字が黒の行(黄色)は調整
                if(rowKey === 's') {
                   k.style.color = "#000"; 
                } else {
                   k.style.filter = "brightness(0.85)"; 
                }
            });
        }
        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;
            
            // clickイベントでPC/スマホ両対応
            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'); // 黄色枠に
            
            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) {
        // 未確定の日本語があれば先に確定させる
        if (composingStr.length > 0) {
            committedStr += composingStr;
            composingStr = "";
        }
        committedStr += char;
        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;
            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