スマホの入力を簡単にする?「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>

コメント