インベントリに戻る
ふりがな入力フック
🔤
ふりがな入力フック
2024エピック
日本語入力時に自動でふりがな(読み仮名)を検知するReactカスタムフック。ローマ字→ひらがな変換テーブルベース。
使用技術
ReactTypeScript
デモ
性(せい)
名(めい)
ふりがな結果
ふりがな結果
useState: {"lastName":"","firstName":"","lastNameFurigana":"","firstNameFurigana":""}
比較分析
| 項目 | vanilla-autokana | useHurigana(自作) |
|---|---|---|
| 方式 | 外部npmライブラリ | Reactカスタムフック |
| 変換原理 | valueポーリング+ひらがなフィルタ | compositionupdate優先+ローマ字変換fallback |
| React互換 | DOM直接操作 (getElementById) | イベントハンドラースプレッド |
| UIライブラリ | 手動連携が必要 | Chakra UI, MUI等と互換 |
| モバイル入力 | 対応(valueポーリング方式) | 対応(compositionupdate) |
| メンテナンス | 2019年以降更新なし | 自分で管理可能 |
| カスタマイズ | 制限的 | テーブル自由に変更可能 |
| カタカナモード | オプション対応 | 内蔵 |
| コピペ | 非対応(keydown基盤) | 非対応(keydown基盤) |
| バンドルサイズ | 外部依存追加 | 依存なし |
| TypeScript | 制限的(v1.1.6) | 完全対応 |
ソースコード
1"use client";23import { useRef, useState, useCallback } from "react";4import type { KeyboardEvent, CompositionEvent, FormEvent } from "react";56interface HuriganaOption {7 katakana?: boolean;8}910function toKatakana(src: string): string {11 return src.replace(/[ぁ-ゖ]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 0x60));12}1314const ROMAJI_TABLE: [string, string][] = [1516 ["xtsu", "っ"], ["ltsu", "っ"],17 ["xkya", "ゃ"], ["xkyu", "ゅ"], ["xkyo", "ょ"],1819 ["sha", "しゃ"], ["shi", "し"], ["shu", "しゅ"], ["sho", "しょ"],20 ["sya", "しゃ"], ["syu", "しゅ"], ["syo", "しょ"],21 ["chi", "ち"], ["cha", "ちゃ"], ["chu", "ちゅ"], ["cho", "ちょ"],22 ["tya", "ちゃ"], ["tyu", "ちゅ"], ["tyo", "ちょ"],23 ["tsu", "つ"],24 ["cya", "ちゃ"], ["cyu", "ちゅ"], ["cyo", "ちょ"],25 ["thi", "てぃ"], ["dhi", "でぃ"], ["dhu", "でゅ"],26 ["tsa", "つぁ"], ["tsi", "つぃ"], ["tse", "つぇ"], ["tso", "つぉ"],27 ["wha", "うぁ"], ["whi", "うぃ"], ["whe", "うぇ"], ["who", "うぉ"],28 ["dzu", "づ"],29 ["kya", "きゃ"], ["kyu", "きゅ"], ["kyo", "きょ"],30 ["nya", "にゃ"], ["nyu", "にゅ"], ["nyo", "にょ"],31 ["hya", "ひゃ"], ["hyu", "ひゅ"], ["hyo", "ひょ"],32 ["mya", "みゃ"], ["myu", "みゅ"], ["myo", "みょ"],33 ["rya", "りゃ"], ["ryu", "りゅ"], ["ryo", "りょ"],34 ["gya", "ぎゃ"], ["gyu", "ぎゅ"], ["gyo", "ぎょ"],35 ["bya", "びゃ"], ["byu", "びゅ"], ["byo", "びょ"],36 ["pya", "ぴゃ"], ["pyu", "ぴゅ"], ["pyo", "ぴょ"],37 ["jya", "じゃ"], ["jyu", "じゅ"], ["jyo", "じょ"],38 ["zya", "じゃ"], ["zyu", "じゅ"], ["zyo", "じょ"],39 ["dya", "ぢゃ"], ["dyu", "ぢゅ"], ["dyo", "ぢょ"],40 ["xtu", "っ"], ["ltu", "っ"],41 ["xya", "ゃ"], ["xyu", "ゅ"], ["xyo", "ょ"],42 ["lya", "ゃ"], ["lyu", "ゅ"], ["lyo", "ょ"],43 ["xwa", "ゎ"],4445 ["ka", "か"], ["ki", "き"], ["ku", "く"], ["ke", "け"], ["ko", "こ"],46 ["sa", "さ"], ["si", "し"], ["su", "す"], ["se", "せ"], ["so", "そ"],47 ["ta", "た"], ["ti", "ち"], ["tu", "つ"], ["te", "て"], ["to", "と"],48 ["na", "な"], ["ni", "に"], ["nu", "ぬ"], ["ne", "ね"], ["no", "の"],49 ["ha", "は"], ["hi", "ひ"], ["hu", "ふ"], ["fu", "ふ"], ["he", "へ"], ["ho", "ほ"],50 ["ma", "ま"], ["mi", "み"], ["mu", "む"], ["me", "め"], ["mo", "も"],51 ["ya", "や"], ["yu", "ゆ"], ["yo", "よ"],52 ["ra", "ら"], ["ri", "り"], ["ru", "る"], ["re", "れ"], ["ro", "ろ"],53 ["wa", "わ"], ["wi", "ゐ"], ["we", "ゑ"], ["wo", "を"],5455 ["ga", "が"], ["gi", "ぎ"], ["gu", "ぐ"], ["ge", "げ"], ["go", "ご"],56 ["za", "ざ"], ["zi", "じ"], ["ji", "じ"], ["zu", "ず"], ["ze", "ぜ"], ["zo", "ぞ"],57 ["ja", "じゃ"], ["ju", "じゅ"], ["jo", "じょ"],58 ["da", "だ"], ["di", "ぢ"], ["du", "づ"], ["de", "で"], ["do", "ど"],59 ["ba", "ば"], ["bi", "び"], ["bu", "ぶ"], ["be", "べ"], ["bo", "ぼ"],60 ["pa", "ぱ"], ["pi", "ぴ"], ["pu", "ぷ"], ["pe", "ぺ"], ["po", "ぽ"],6162 ["ye", "いぇ"],63 ["fa", "ふぁ"], ["fi", "ふぃ"], ["fe", "ふぇ"], ["fo", "ふぉ"],64 ["va", "ゔぁ"], ["vi", "ゔぃ"], ["vu", "ゔ"], ["ve", "ゔぇ"], ["vo", "ゔぉ"],6566 ["xa", "ぁ"], ["xi", "ぃ"], ["xu", "ぅ"], ["xe", "ぇ"], ["xo", "ぉ"],67 ["la", "ぁ"], ["li", "ぃ"], ["lu", "ぅ"], ["le", "ぇ"], ["lo", "ぉ"],68 ["nn", "ん"],6970 ["a", "あ"], ["i", "い"], ["u", "う"], ["e", "え"], ["o", "お"],71 ["n", "ん"],72];7374const DOUBLE_CONSONANTS = new Set([75 "bb", "cc", "dd", "ff", "gg", "hh", "jj", "kk", "ll", "mm",76 "pp", "rr", "ss", "tt", "vv", "ww", "zz",77]);7879function romajiToHiragana(romaji: string): string {80 let result = "";81 let buf = romaji.toLowerCase();8283 while (buf.length > 0) {8485 if (86 buf[0] === "n" &&87 buf.length >= 2 &&88 buf[1] !== "a" && buf[1] !== "i" && buf[1] !== "u" &&89 buf[1] !== "e" && buf[1] !== "o" && buf[1] !== "y" && buf[1] !== "n"90 ) {91 result += "ん";92 buf = buf.slice(1);93 continue;94 }9596 if (buf.length >= 2 && DOUBLE_CONSONANTS.has(buf.slice(0, 2))) {97 result += "っ";98 buf = buf.slice(1);99 continue;100 }101102 let matched = false;103 for (const [rom, hira] of ROMAJI_TABLE) {104 if (buf.startsWith(rom)) {105 result += hira;106 buf = buf.slice(rom.length);107 matched = true;108 break;109 }110 }111112 if (!matched) {113 buf = buf.slice(1);114 }115 }116117 return result;118}119120export function useHurigana(option?: HuriganaOption) {121122 const [furigana, setFurigana] = useState("");123124 const romajiBufferRef = useRef("");125 const compositionKanaRef = useRef("");126 const maxDataLenRef = useRef(0);127 const hadBackspaceRef = useRef(false);128 const confirmedRef = useRef<string[]>([]);129130 const segmentsRef = useRef<{ input: string; kana: string }[]>([]);131132 const prevInputLenRef = useRef(0);133134 function clearBuffers() {135 romajiBufferRef.current = "";136 compositionKanaRef.current = "";137 maxDataLenRef.current = 0;138 hadBackspaceRef.current = false;139 }140141 const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {142143 if (e.metaKey || e.ctrlKey || e.altKey) return;144145 if (e.key === "Backspace") {146147 romajiBufferRef.current = romajiBufferRef.current.slice(0, -1);148 } else if (e.key.length === 1 && /[a-z]/i.test(e.key)) {149150 romajiBufferRef.current += e.key.toLowerCase();151 } else if (e.code && /^Key[A-Z]$/.test(e.code)) {152153 romajiBufferRef.current += e.code.charAt(3).toLowerCase();154 }155 }, []);156157 const onCompositionUpdate = useCallback((e: CompositionEvent<HTMLInputElement>) => {158159 const kana = e.data.replace(/[^ぁ-んー]/g, "");160161 const hasKanji = /[一-龯]/.test(e.data);162163 maxDataLenRef.current = Math.max(maxDataLenRef.current, e.data.length);164165 if (!hasKanji && e.data.length < maxDataLenRef.current) {166167 compositionKanaRef.current = kana;168 maxDataLenRef.current = e.data.length;169 hadBackspaceRef.current = true;170 } else if (kana && kana.length >= compositionKanaRef.current.length) {171172 compositionKanaRef.current = kana;173 }174175 }, []);176177 const onCompositionEnd = useCallback((e: CompositionEvent<HTMLInputElement>) => {178179 let kana = compositionKanaRef.current;180181 if (romajiBufferRef.current && !hadBackspaceRef.current) {182 const romajiKana = romajiToHiragana(romajiBufferRef.current);183184 if (romajiKana.length > kana.length) {185 kana = romajiKana;186 }187 }188189 if (kana) {190191 const confirmedInput = e.data || "";192193 const actualLen = e.currentTarget.value.length;194195 const expectedLen = segmentsRef.current.reduce((sum, s) => sum + s.input.length, 0) + confirmedInput.length;196197 if (actualLen !== expectedLen) {198 segmentsRef.current = [];199 confirmedRef.current = [];200 }201202 segmentsRef.current.push({ input: confirmedInput, kana });203 confirmedRef.current.push(kana);204205 prevInputLenRef.current = actualLen;206207 const joined = confirmedRef.current.join("");208 setFurigana(option?.katakana ? toKatakana(joined) : joined);209 }210211 clearBuffers();212 }, [option?.katakana]);213214 const onInput = useCallback((e: FormEvent<HTMLInputElement>) => {215 const nativeEvent = e.nativeEvent as InputEvent;216217 if (nativeEvent.isComposing) return;218219 romajiBufferRef.current = "";220221 const currentValue = e.currentTarget.value;222223 if (currentValue === "") {224 confirmedRef.current = [];225 segmentsRef.current = [];226 prevInputLenRef.current = 0;227 clearBuffers();228 setFurigana("");229 return;230 }231232 if (currentValue.length < prevInputLenRef.current) {233 const deletedCount = prevInputLenRef.current - currentValue.length;234 let remaining = deletedCount;235236 while (remaining > 0 && segmentsRef.current.length > 0) {237 const lastSeg = segmentsRef.current[segmentsRef.current.length - 1];238239 if (remaining >= lastSeg.input.length) {240241 remaining -= lastSeg.input.length;242 segmentsRef.current.pop();243 confirmedRef.current.pop();244 } else {245246 if (lastSeg.input === lastSeg.kana) {247248 const prevTotal = segmentsRef.current.slice(0, -1)249 .reduce((sum, s) => sum + s.input.length, 0);250 const actualText = currentValue.slice(prevTotal);251 lastSeg.input = actualText;252 lastSeg.kana = actualText;253 confirmedRef.current[confirmedRef.current.length - 1] = actualText;254 }255256 remaining = 0;257 }258 }259260 prevInputLenRef.current = currentValue.length;261 const joined = confirmedRef.current.join("");262 setFurigana(option?.katakana ? toKatakana(joined) : joined);263 } else {264265 prevInputLenRef.current = currentValue.length;266 }267 }, [option?.katakana]);268269 const resetFurigana = useCallback(() => {270 confirmedRef.current = [];271 segmentsRef.current = [];272 prevInputLenRef.current = 0;273 clearBuffers();274 setFurigana("");275 }, []);276277 const handlers = { onKeyDown, onCompositionUpdate, onCompositionEnd, onInput };278279 return { handlers, furigana, resetFurigana };280}