Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(search-input, vue): 検索改善 #1338

Merged
merged 3 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silly-walls-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wizleap-inc/wiz-ui-next": minor
---

Fix(search-input): 検索機能を改善
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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<Record<string, number>>(
(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]
)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<Emit>();
Expand All @@ -225,26 +237,6 @@ const toggleDropdown = () => {

const deepCopy = <T>(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;
Expand All @@ -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);
Expand Down
Loading