2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript 2Advent Calendar 2019

Day 13

EasyAutocompleteを日本語対応っぽくする

Last updated at Posted at 2019-12-12

投稿日、13日の金曜日やんけ~~~

0.前置き

autocompleteを実装するために、jQueryライブラリの「EasyAutocomplete」を使用することにしました。

EasyAutocomplete - jQuery autocomplete plugin

1.問題点

ローマ字でサジェストを出す前提で作られているため、部分一致していないとサジェストを出してくれません。
 → 漢字の候補を用意しただけでは、ローマ字・ひらがな・かたかなの「読み」での検索ができない

今回は都道府県の入力を補完してほしかったので、その機能を補完する設定をまとめてみました。

2.できたもの

ひらがな・カタカナ・ローマ字で都道府県の検索ができます。
(右側はデフォルトのテキストボックスです。)

See the Pen auto suggest by かぶきち (@cubkich) on CodePen.

3.HTML

CodePenでは記載がないですが、動かすためにはjQueryとEasyAutocompleteを先に読み込む必要があります。

index.html
<head>
    <!-- 省略 -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/easy-autocomplete/1.3.5/jquery.easy-autocomplete.min.js"></script>
    <!-- 省略 -->
</head>

サイトに合わせて適宜フォーム等を設置します。

index.html
<form>
	<div>
		<label>
			都道府県			
			<input id="autocomplete">
		</label>
		<label>
			自由入力<br>
			<input type="text" placeholder="自由入力">
		</label>
	</div>
	<button type="submit"> 探す</button>
</form>

今回は普通ののテキストボックス(自由入力)もあります。
(掛け合わせ検索をする前提で作った)

4.CSS

特にMUSTのものは無いので、任意でスタイルをつけてください。
サジェスト部分のリストのスタイルは.easy-autocomplete-container ul liとかで設定できると思います(適当)。

5.JS

大まかな流れ

流れとしては、自作のjQueryプラグインを作り、その中で本家のEasyAutocompleteを実行する形になります。
入力に対してJSONから一致する項目を検索するのですが、「ひらがな」、「カタカナ」、「ローマ字」をそれぞれ登録するのは 面倒くさい 更新性が落ちてしまうので、、カタカナのみを登録し、そこから検索できるようにしています。(JSONは後ほど紹介します)

jquery.easy-autocomplete-extender.js
/* 大まかな流れ */
(function($) {
	/*
		ここで入力文字列を加工して必要なオプションを設定する
			入力文字列→カタカナに変換
			カタカナの読み方から検索→リストを表示
	*/
})(jQuery);

$(function() {
	const options = {
		placeholder: "【プレースホルダーのテキスト(任意)】"
	}
	/* 本家プラグインの実行 */
	$("【対象のセレクタ】").areaSuggest("【使用するjsonファイルへのパス】", options);
});

ひらがなをカタカナに変換

この関数を通すことで入力されたひらがなはカタカナに変換されます。

jquery.easy-autocomplete-extender.js
function hiraToKana(str) {
	return str.replace(/[\u3041-\u3096]/g, function(match) {
		var chr = match.charCodeAt(0) + 0x60;
		return String.fromCharCode(chr);
	});
}

ローマ字をカタカナに変換

@recordare さんのJavaScriptでローマ字をカタカナに変換する関数を参考にさせていただきました。
(先人の知恵、ありがとうございます…ありがとうございます…)
(TS→JSにコンパイルして使用しています。)

この関数を通すことで、入力されたローマ字はカタカナに変換されます。

jquery.easy-autocomplete-extender.js
const tree = {
	a: '', i: '', u: '', e: '', o: '',
	// ~~~
};
function convertRomanToKana(original) {
	const str = original.replace(/[A-Za-z]/, function(s) {String.fromCharCode(s.charCodeAt(0) - 65248)}).toLowerCase(), // 全角→半角→小文字
		len = str.length;
	let result = '',
		tmp = '',
		index = 0,
		node = tree;
	const push = function(char, toRoot) {
		toRoot = toRoot ? toRoot : true;
		result += char;
		tmp = '';
		node = toRoot ? tree : node;
	};
	while (index < len) {
		const char = str.charAt(index);
		if (char.match(/[a-z]/)) { // 英数字以外は考慮しない
			if (char in node) {
				const next = node[char];
				if (typeof next === 'string') {
					push(next);
				} else {
					tmp += original.charAt(index);
					node = next;
				}
				index++;
				continue;
			}
			const prev = str.charAt(index - 1);
			if (prev && (prev === 'n' || prev === char)) { // 促音やnへの対応
				push(prev === 'n' ? '' : '', false);
			}
			if (node !== tree && char in tree) { // 今のノードがルート以外だった場合、仕切り直してチェックする
				push(tmp);
				continue;
			}
		}
		push(tmp + char);
		index++;
	}
	tmp = tmp.replace(/n$/, ''); // 末尾のnは変換する
	push(tmp);
	return result;
}

一旦まとめ

ここまで、EasyAutocompleteのオプション部分で使用する関数をつらつらと書いていきましたので、一旦それらを使ってまとめてみます。

jquery.easy-autocomplete-extender.js
/*
* [must] path: jsonまでのパスを入力します
* [any] options: 本家easyAutocompleteで使用できるoptionを設定できます
*/
$.fn.areaSuggest = function(path, options) {
	const _self = $(this);
	options = options || null;

	$.ajax({
		url: path,
		dataType: "json",
	}).done(function(data) {
		const default_options = {
			data: data,
			getValue: function(elm) {
				return elm.name;
			},
			list: {
				match: {
					enabled: true,
					method: function(name, phrase) {
						let input = phrase;
						if(phrase.match(/^[ぁ-んー]*$/)) {
							// 入力がひらがなのとき、ひらがな→カタカナ
							input = hiraToKana(phrase);
						} else if(phrase.match(/^[a-zA-Z]*$/)) {
							// 入力がローマ字のとき、ローマ字→カタカナ
							input = convertRomanToKana(phrase);
						}
						// ひらがなでもローマ字でもないとき、そのまま

						const thisData = $.grep(data,
							function(elm, i) {
								if(elm.name === name) {
									return elm;
								}
							});

						// カタカナのときはカタカナで検索、それ以外はそのまま検索
						return input.match(/^[ァ-ンヴー]*$/)
							? thisData[0].kana.indexOf(input) > -1
							: thisData[0].name.indexOf(input) > -1;
					},
				},
				// 10件まで表示する
				maxNumberOfElements: 10,
			},
			// 入力からから10ミリ秒後に実行
			requestDelay: 400,
			placeholder: "地域を入力してください",
		};
		// オプションが入力されていれば、その内容を書き換え
		if(options) {
			$.each(options, function(i, val) {
				default_options[i] = val;
			});
		}

		// 本家easyAutocompleteの実行
		_self.easyAutocomplete(default_options);
		return _self;
	}).fail(function(data) {
		console.error("ERROR: ajax failed");
	});
}

JSON

検索対象のJSONファイルです。
一応、ベタ書きやxmlでも動くようですが、今回はJSONで準備しています。

入力がカタカナに変換されたときは、"kana"を検索します。
入力が変換されずにそのままのときは、"name"を検索します。

sample.json
[
	{"name": "北海道", "kana": "ホッカイドウ"},
	{"name": "青森県", "kana": "アオモリケン"},
	{"name": "岩手県", "kana": "イワテケン"},
	{"name": "宮城県", "kana": "ミヤギケン"},
	{"name": "秋田県", "kana": "アキタケン"},
	// 続く…
]

まとめ

そのままでは文字の完全一致?しかマッチしなかったeasyAutocompleteが日本語にも対応できるようになりました。
もともと英語圏での使用が想定されていたと思うので、アルファベットの場合はそれだけで十分でしたが、
日本語のひらがな・カタカナ・漢字も網羅できるようになり、使える幅がぐっと広がったような気がします。

サジェスト機能を使いたいけど、Googleカスタム検索とかそこまでじゃないんだよなぁ…ってときに使えるかなぁと。

まぁ、都道府県に限った話でいうと、チェックボックスとかセレクトボックスで良いじゃん?となるかもしれませんが、毎回下の方まで移動するという手間は省けるのではないでしょうか。
(私は比較的上の方なのそんなに困ってはいないのですが笑)

最後に、拡張分のjquery.easy-autocomplete-extender.jsのソースを張っておしまいです。(長い)

jquery.easy-autocomplete-extender.js
/* jquery.easy-autocomplete-extender.js */
(function($) {
	$.fn.areaSuggest = function(path, options) {
		const _self = $(this);
		options = options ? options : null;
		$.ajax({
			url: path,
			dataType: "json",
		}).done(function(data) {
			const default_options = {
				data: data,
				getValue: function(elm) {
					return elm.name;
				},
				list: {
					match: {
						enabled: true,
						method: function(name, phrase) {
							let input = phrase;
							if(phrase.match(/^[ぁ-んー]*$/)) {
								input = hiraToKana(phrase);
							} else if(phrase.match(/^[a-zA-Z]*$/)) {
								input = convertRomanToKana(phrase);
							}

							const thisData = $.grep(data,
								function(elm, i) {
									if(elm.name === name) {
										return elm;
									}
								});

							return input.match(/^[ァ-ンヴー]*$/)
								? thisData[0].kana.indexOf(input) > -1
								: thisData[0].name.indexOf(input) > -1;
						},
					},
					maxNumberOfElements: 10,
				},
				requestDelay: 400,
				placeholder: "地域を入力してください",
			};
			if(options) {
				$.each(options, function(i, val) {
					default_options[i] = val;
				});
			}

			_self.easyAutocomplete(default_options);
			return _self;
		}).fail(function(data) {
			console.error("ERROR: ajax failed");
		});
	}

	const tree = {
		a: '', i: '', u: '', e: '', o: '',
		k: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'キャ', i: 'キィ', u: 'キュ', e: 'キェ', o: 'キョ' },
		},
		s: {
			a: '', i: '', u: '', e: '', o: '',
			h: { a: 'シャ', i: '', u: 'シュ', e: 'シェ', o: 'ショ' },
			y: { a: 'キャ', i: 'キィ', u: 'キュ', e: 'キェ', o: 'キョ' },
		},
		t: {
			a: '', i: '', u: '', e: '', o: '',
			h: { a: 'テャ', i: 'ティ', u: 'テュ', e: 'テェ', o: 'テョ' },
			y: { a: 'チャ', i: 'チィ', u: 'チュ', e: 'チェ', o: 'チョ' },
			s: { a: 'ツァ', i: 'ツィ', u: '', e: 'ツェ', o: 'ツォ' },
		},
		c: {
			a: '', i: '', u: '', e: '', o: '',
			h: { a: 'チャ', i: '', u: 'チュ', e: 'チェ', o: 'チョ' },
			y: { a: 'チャ', i: 'チィ', u: 'チュ', e: 'チェ', o: 'チョ' },
		},
		q: {
			a: 'クァ', i: 'クィ', u: '', e: 'クェ', o: 'クォ',
		},
		n: {
			a: '', i: '', u: '', e: '', o: '', n: '',
			y: { a: 'ニャ', i: 'ニィ', u: 'ニュ', e: 'ニェ', o: 'ニョ' },
		},
		h: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'ヒャ', i: 'ヒィ', u: 'ヒュ', e: 'ヒェ', o: 'ヒョ' },
		},
		f: {
			a: 'ファ', i: 'フィ', u: '', e: 'フェ', o: 'フォ',
			y: { a: 'フャ', u: 'フュ', o: 'フョ' },
		},
		m: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'ミャ', i: 'ミィ', u: 'ミュ', e: 'ミェ', o: 'ミョ' },
		},
		y: { a: '', i: '', u: '', e: 'イェ', o: '' },
		r: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'リャ', i: 'リィ', u: 'リュ', e: 'リェ', o: 'リョ' },
		},
		w: { a: '', i: 'ウィ', u: '', e: 'ウェ', o: '' },
		g: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'ギャ', i: 'ギィ', u: 'ギュ', e: 'ギェ', o: 'ギョ' },
		},
		z: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'ジャ', i: 'ジィ', u: 'ジュ', e: 'ジェ', o: 'ジョ' },
		},
		j: {
			a: 'ジャ', i: '', u: 'ジュ', e: 'ジェ', o: 'ジョ',
			y: { a: 'ジャ', i: 'ジィ', u: 'ジュ', e: 'ジェ', o: 'ジョ' },
		},
		d: {
			a: '', i: '', u: '', e: '', o: '',
			h: { a: 'デャ', i: 'ディ', u: 'デュ', e: 'デェ', o: 'デョ' },
			y: { a: 'ヂャ', i: 'ヂィ', u: 'ヂュ', e: 'ヂェ', o: 'ヂョ' },
		},
		b: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'ビャ', i: 'ビィ', u: 'ビュ', e: 'ビェ', o: 'ビョ' },
		},
		v: {
			a: 'ヴァ', i: 'ヴィ', u: '', e: 'ヴェ', o: 'ヴォ',
			y: { a: 'ヴャ', i: 'ヴィ', u: 'ヴュ', e: 'ヴェ', o: 'ヴョ' },
		},
		p: {
			a: '', i: '', u: '', e: '', o: '',
			y: { a: 'ピャ', i: 'ピィ', u: 'ピュ', e: 'ピェ', o: 'ピョ' },
		},
		x: {
			a: '', i: '', u: '', e: '', o: '',
			y: {
				a: '', i: '', u: '', e: '', o: '',
			},
			t: {
				u: '',
				s: {
					u: '',
				},
			},
		},
		l: {
			a: '', i: '', u: '', e: '', o: '',
			y: {
				a: '', i: '', u: '', e: '', o: '',
			},
			t: {
				u: '',
				s: {
					u: '',
				},
			},
		},
	};
	function convertRomanToKana(original) {
		const str = original.replace(/[A-Za-z]/, function(s) {String.fromCharCode(s.charCodeAt(0) - 65248)}).toLowerCase(),
			len = str.length;
		let result = '',
			tmp = '',
			index = 0,
			node = tree;
		const push = function(char, toRoot) {
			toRoot = toRoot ? toRoot : true;
			result += char;
			tmp = '';
			node = toRoot ? tree : node;
		};
		while (index < len) {
			const char = str.charAt(index);
			if (char.match(/[a-z]/)) {
				if (char in node) {
					const next = node[char];
					if (typeof next === 'string') {
						push(next);
					} else {
						tmp += original.charAt(index);
						node = next;
					}
					index++;
					continue;
				}
				const prev = str.charAt(index - 1);
				if (prev && (prev === 'n' || prev === char)) {
					push(prev === 'n' ? '' : '', false);
				}
				if (node !== tree && char in tree) {
					push(tmp);
					continue;
				}
			}
			push(tmp + char);
			index++;
		}
		tmp = tmp.replace(/n$/, '');
		push(tmp);
		return result;
	}

	function hiraToKana(str) {
		return str.replace(/[\u3041-\u3096]/g, function(match) {
			var chr = match.charCodeAt(0) + 0x60;
			return String.fromCharCode(chr);
		});
	}
})(jQuery);

読み込むHTML側での実行等をお忘れなく

index.html
<script>
$(function() {
	const options = {
		placeholder: "都道府県"
	}
});
</script>
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?