Skip to content

Commit

Permalink
Merge pull request #1338 from Wizleap-Inc/fix/search-selector-filter-vue
Browse files Browse the repository at this point in the history
fix(search-input, vue): 検索改善
  • Loading branch information
ichi-h authored Jul 11, 2024
2 parents 875a1dc + 9a075ce commit 28eb127
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 29 deletions.
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 @@ -338,7 +338,7 @@ export const ExlabelWithoutShowExlabel = Template(
[],
true,
_getDummyOptions("test", 3, "(10)"),
"new option"
""
).bind({});
ExlabelWithoutShowExlabel.args = {
addable: true,
Expand All @@ -358,7 +358,7 @@ export const Exlabel = Template(
[],
true,
_getDummyOptions("test", 3, "(10)"),
"new option"
""
).bind({});
Exlabel.args = {
addable: true,
Expand All @@ -378,7 +378,7 @@ export const ExlabelWithLongLabel = Template(
[],
true,
_getDummyOptions("testtesttesttesttesttesttesttesttesttest", 3, "(10)"),
"new option"
""
).bind({});
ExlabelWithLongLabel.args = {
addable: true,
Expand Down
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

0 comments on commit 28eb127

Please sign in to comment.