From df55d3a73e45a9aaa5e4ec172bc76ade69ced12e Mon Sep 17 00:00:00 2001 From: AOKI Takashi <55625375+RyushiAok@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:19:58 +0900 Subject: [PATCH 1/3] fix(search-input, vue): filter --- .../search-selector/levenshtein-distance.ts | 37 ++++++++++++++ .../search-selector/search-selector.vue | 48 +++++++++---------- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/packages/wiz-ui-next/src/components/base/inputs/search-selector/levenshtein-distance.ts b/packages/wiz-ui-next/src/components/base/inputs/search-selector/levenshtein-distance.ts index e6fcc0992..cdd79865a 100644 --- a/packages/wiz-ui-next/src/components/base/inputs/search-selector/levenshtein-distance.ts +++ b/packages/wiz-ui-next/src/components/base/inputs/search-selector/levenshtein-distance.ts @@ -1,3 +1,5 @@ +import { SelectBoxOption } from "./types"; + export const levenshteinDistance = (s1: string, s2: string): number => { const dist: number[][] = Array.from({ length: s1.length + 1 }, () => Array(s2.length + 1).fill(0) @@ -16,3 +18,38 @@ export const levenshteinDistance = (s1: string, s2: string): number => { } return dist[s1.length][s2.length] / Math.max(s1.length, s2.length); }; + +export function filterOptions( + options: SelectBoxOption[], + searchText: string, + threshold: number +) { + if (searchText.length === 0) { + return options; + } + const levenshteinDistanceMap = options.reduce>( + (map, option) => { + map[option.label] = levenshteinDistance(option.label, searchText); + return map; + }, + {} + ); + const minLevenshteinDistance = Math.min( + ...Object.values(levenshteinDistanceMap) + ); + return ( + options + // 類似度が閾値以下 or 全て閾値を上回る場合は部分一致 + .filter( + (option) => + levenshteinDistanceMap[option.label] <= threshold || + (minLevenshteinDistance > threshold && + option.label.includes(searchText)) + ) + // 類似度でソート + .sort( + (a, b) => + levenshteinDistanceMap[a.label] - levenshteinDistanceMap[b.label] + ) + ); +} diff --git a/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.vue b/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.vue index 8535ef40b..305cabcd5 100644 --- a/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.vue +++ b/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.vue @@ -131,7 +131,7 @@ import { formControlKey } from "@/hooks/use-form-control-provider"; import { WizHStack, WizVStack } from "../../stack"; -import { levenshteinDistance } from "./levenshtein-distance"; +import { filterOptions } from "./levenshtein-distance"; import { ButtonGroupItem, PopupButtonGroup } from "./popup-button-group"; import { SelectBoxOption } from "./types"; @@ -207,6 +207,18 @@ const props = defineProps({ type: String, required: false, }, + /** + * 検索対象に含む類似度の閾値を,0から1の範囲で指定します。 + * 類似度は標準化レーベンシュタイン距離に基づいて計算され,0に近いほど類似しています。 + * ただし,類似度の最小値が閾値を上回る場合は部分一致で検索します。 + * @default 0.75 + * + */ + threshold: { + type: Number, + required: false, + default: 0.75, + }, }); const emit = defineEmits(); @@ -225,26 +237,6 @@ const toggleDropdown = () => { const deepCopy = (ary: T): T => JSON.parse(JSON.stringify(ary)); -const selectByLevenshteinAndPartialMatch = ( - options: SelectBoxOption[], - target: string -) => { - const dist = options.reduce((acc, str) => { - acc[str.label] = levenshteinDistance(str.label, target); - return acc; - }, {} as { [key: string]: number }); - const minLength = Math.min(...Object.values(dist)); - const closestWords = options.filter( - (option) => dist[option.label] === minLength - ); - - const exactMatch = options.filter((option) => { - const isIncluded = option.label.indexOf(target) !== -1; - return isIncluded && !closestWords.includes(option); - }); - return closestWords.concat(exactMatch); -}; - const valueToOption = computed(() => props.options.reduce((acc, item) => { acc[item.value] = item; @@ -268,12 +260,16 @@ const setUnselectableRef = const filteredOptions = computed(() => { const sortedOptions = - props.searchValue.length !== 0 - ? selectByLevenshteinAndPartialMatch( + props.searchValue.length === 0 + ? props.options + : filterOptions( deepCopy(props.options), - props.searchValue - ) - : props.options; + props.searchValue, + props.threshold + ).filter( + (matchedOption) => + !props.modelValue.some((value) => matchedOption.value === value) + ); const removeSelectedOptions = (options: SelectBoxOption[]) => { return options.filter((v) => { return !selectedItem.value.some((item) => item.value === v.value); From 3cf02c14e7415417520b62cad717c987b5218b9c Mon Sep 17 00:00:00 2001 From: AOKI Takashi <55625375+RyushiAok@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:22:12 +0900 Subject: [PATCH 2/3] chore(search-selector): add changeset --- .changeset/silly-walls-shave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-walls-shave.md diff --git a/.changeset/silly-walls-shave.md b/.changeset/silly-walls-shave.md new file mode 100644 index 000000000..1d1ebfc86 --- /dev/null +++ b/.changeset/silly-walls-shave.md @@ -0,0 +1,5 @@ +--- +"@wizleap-inc/wiz-ui-next": minor +--- + +Fix(search-input): 検索機能を改善 From 9a075ce02607e30c424880dadb6887d3829fb0d2 Mon Sep 17 00:00:00 2001 From: AOKI Takashi <55625375+RyushiAok@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:18:08 +0900 Subject: [PATCH 3/3] docs(search-selector): update stories --- .../base/inputs/search-selector/search-selector.stories.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.stories.ts b/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.stories.ts index 2925c1d0c..7af4ea4d7 100644 --- a/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.stories.ts +++ b/packages/wiz-ui-next/src/components/base/inputs/search-selector/search-selector.stories.ts @@ -338,7 +338,7 @@ export const ExlabelWithoutShowExlabel = Template( [], true, _getDummyOptions("test", 3, "(10)"), - "new option" + "" ).bind({}); ExlabelWithoutShowExlabel.args = { addable: true, @@ -358,7 +358,7 @@ export const Exlabel = Template( [], true, _getDummyOptions("test", 3, "(10)"), - "new option" + "" ).bind({}); Exlabel.args = { addable: true, @@ -378,7 +378,7 @@ export const ExlabelWithLongLabel = Template( [], true, _getDummyOptions("testtesttesttesttesttesttesttesttesttest", 3, "(10)"), - "new option" + "" ).bind({}); ExlabelWithLongLabel.args = { addable: true,