0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

薬剤師のためのautocompleteを、結局初めから作ることになった話

Posted at

薬剤師のためのautocompleteを、結局初めから作ることになった話

作りたいもの

薬歴を書く際に、薬品名を簡単に入力するための補助アプリを作りたいと考えました。以下の機能を目指します。

  • インプットに薬品名の数文字を入力すると、自動的に候補が補完される。
  • キー操作でクリップボードに文字列をコピーできる。
  • オフライン環境でも動作する。

基本戦略

  • 技術スタック: Electron + Vite + React
  • 言語: TypeScript

問題

  • ネットで調べた結果、<input> と <datalist> を組み合わせたシンプルな実装が便利そうだったので、試してみました。しかし、以下の問題が発生しました。

  • 候補数が多すぎてレイアウトが崩れる。

医薬品名は似たような名前が多く、特にジェネリック医薬品は種類が多いです。 例えば、ドネペジルという成分名の薬だけで123品目もあります。 結果として、\ を使うと候補が大量に表示され、レイアウトが崩れ、最悪の場合クラッシュしてしまうことが判明しました。

例:

ドネペジル塩酸塩0.5%細粒
ドネペジル塩酸塩3mg錠
ドネペジル塩酸塩錠3mg「DSEP」
ドネペジル塩酸塩錠3mg「FFP」
ドネペジル塩酸塩錠3mg「JG」
ドネペジル塩酸塩錠3mg「NP」
ドネペジル塩酸塩錠3mg「TSU」
ドネペジル塩酸塩錠3mg「アメル」
ドネペジル塩酸塩錠3mg「オーハラ」
ドネペジル塩酸塩錠3mg「杏林」
ドネペジル塩酸塩錠3mg「ケミファ」
ドネペジル塩酸塩錠3mg「サワイ」
…(以下略)
  • <datalist> では柔軟な部分一致検索が難しい。
    例えば「ドネぺ3」と入力しても、期待した候補が表示されない。

いくつかのautocomple系ライブラリを試してみたが、私の技術では、うまくいかなかった。

開発方針

  • 入力したキーワード(例:「ドネぺ3」)に対応するため、検索ロジックを自作する。
  • 候補が長くなってもレイアウトが崩れないよう、スクロールバーを付ける。
  • <datalist> に似た使用感を維持する。

実装

検索

  • ライブラリ:fuse.js

  • oprion: 3文字以上で検索を開始

  • 手順:検索文字を一文字ずつ分解して、順次マッチする文字列をピックアップしていく

    • // 検索の開始地点
      public matchingbyEachCharactor(query: string) {
      	// queryの正規化
      	const normalized = this.normalizeQuery(query); 
      	const chars = normalized.split("");
      	for (const c of chars) {
      		this.matchingCore(this.selectedDatas, c);
      		this.selectedDatas = this.extractSuffixByKeyword(
      			c,
      			this.selectedDatas,
      		);
      	}
      }
      
      protected matchingCore(_data: DrugData[], query: string) {
      	const normalized = this.normalizeQuery(query);
      	const fuse = new Fuse(_data, this.options);
      	this.selectedDatas = fuse.search(normalized).map((r) => r.item);
      }
      
      protected extractSuffixByKeyword(
      	keyword: string,
      	_datas: DrugData[],
      ): DrugData[] {
      	const normalized = this.normalizeQuery(keyword);
      	
      	// ここで、正規表現を使って、アルファベットのみかどうかを判定
      	const isAlphabet = /^[a-zA-Z]+$/.test(normalized);
      
      	return _datas.map((d) => {
      		
      		let newData= "";
      		let index = -1;
      		if (isAlphabet) {
      			const normalizedLowerCase = normalized.toLowerCase();
      			index = d.val.toLowerCase().indexOf(normalizedLowerCase);
      			
      		} else {
      			index = d.val.indexOf(normalized);
      		}
      		if (index !== -1) {
      			newData = d.val.substring(index + 1);
      		} 
      
      		return {
      			...d,
      			val: newData,
      		};
      	});
      }
      

表示部分

  • <select> を使用して候補リストを表示
  • スクロールバーで候補を快適に閲覧可能にする。

キーバインド実装

  • onKeyDown イベントを使用してキー入力に対応

  • 変換中の入力と、リストハイライト用のキー操作を分離して処理する

     <input
     	ref={inputRef}
     	id="drugName"
     	type="text"
     	value={searchTerm}
     	onChange={handleInput}
     	onKeyDown={handleKeyDown}
     	onCompositionEnd={compositionEndHandler}
     	onCompositionStart={() => setDidEndComposition(false)}
     	className="w-full px-3 py-2 border border-gray-300 rounded 
     	focus:outline focus:outline-1 focus:outline-white"
     	placeholder="入力してください"
     />
    
  • つまづきポイント

    • 入力中の変換キーイベントと、候補リスト移動用のキーイベントを区別する必要があった。
      • onCompositionStart と onCompositionEnd を活用して状態を適切に管理。

成果物

  • demo_20240223.gif
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?