feat(grid): enhance field search and filter interaction
This commit is contained in:
26 files changed
+491
-35
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
|
||||
|
||||
/**
|
||||
* Scores identifier matches for completion and field pickers.
|
||||
* Higher scores are better; -1 means there is no match.
|
||||
*/
|
||||
export function identifierMatchScore(candidate: string, query: string): number {
|
||||
if (!query) return 1;
|
||||
const candidateLower = candidate.toLowerCase();
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
if (candidateLower === queryLower) return 3000 - candidateLower.length;
|
||||
if (candidateLower.startsWith(queryLower)) return 2000 - candidateLower.length;
|
||||
|
||||
const initials = identifierInitials(candidate);
|
||||
if (initials && initials.startsWith(queryLower)) {
|
||||
const exactInitialsBonus = initials === queryLower ? 400 : 0;
|
||||
return 2400 + exactInitialsBonus - candidateLower.length;
|
||||
}
|
||||
|
||||
if (/^[a-z0-9]+$/.test(queryLower) && containsHan(candidateLower)) {
|
||||
const pinyinInitials = pinyinFirstLetters(candidateLower);
|
||||
if (pinyinInitials.startsWith(queryLower)) {
|
||||
const exactInitialsBonus = pinyinInitials === queryLower ? 300 : 0;
|
||||
return 2300 + exactInitialsBonus - candidateLower.length;
|
||||
}
|
||||
const subsequence = orderedSubsequenceSpan(pinyinInitials, queryLower);
|
||||
if (subsequence) {
|
||||
return 1600 - subsequence.first * 30 - (subsequence.span - queryLower.length) * 10 - candidateLower.length;
|
||||
}
|
||||
}
|
||||
|
||||
const substringIndex = candidateLower.indexOf(queryLower);
|
||||
if (substringIndex >= 0) {
|
||||
const boundaryBonus = isIdentifierBoundary(candidate, substringIndex) ? 400 : Math.max(0, 180 - substringIndex * 12);
|
||||
return 900 + boundaryBonus - candidateLower.length;
|
||||
}
|
||||
|
||||
let candidateIndex = 0;
|
||||
let totalGap = 0;
|
||||
let firstMatchPosition = -1;
|
||||
let boundaryBonus = 0;
|
||||
for (const character of queryLower) {
|
||||
const nextPosition = candidateLower.indexOf(character, candidateIndex);
|
||||
if (nextPosition === -1) return -1;
|
||||
if (firstMatchPosition === -1) firstMatchPosition = nextPosition;
|
||||
if (isIdentifierBoundary(candidate, nextPosition)) boundaryBonus += 40;
|
||||
totalGap += nextPosition - candidateIndex;
|
||||
candidateIndex = nextPosition + 1;
|
||||
}
|
||||
|
||||
const earlyMatchBonus = Math.max(0, 700 - firstMatchPosition * 35) + boundaryBonus;
|
||||
if (totalGap >= queryLower.length) {
|
||||
return 400 + earlyMatchBonus * 0.3 - totalGap * 20 - candidateLower.length;
|
||||
}
|
||||
return 1200 + earlyMatchBonus - totalGap * 10 - candidateLower.length;
|
||||
}
|
||||
|
||||
export function matchesIdentifierSearch(candidate: string, query: string): boolean {
|
||||
return identifierMatchScore(candidate, query.trim()) >= 0;
|
||||
}
|
||||
|
||||
function identifierWords(candidate: string): string[] {
|
||||
return candidate
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9]+/)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function identifierInitials(candidate: string): string {
|
||||
return identifierWords(candidate)
|
||||
.map((part) => part[0])
|
||||
.join("");
|
||||
}
|
||||
|
||||
function isIdentifierBoundary(candidate: string, index: number): boolean {
|
||||
if (index <= 0) return true;
|
||||
const previous = candidate[index - 1] ?? "";
|
||||
const current = candidate[index] ?? "";
|
||||
return /[^A-Za-z0-9]/.test(previous) || (/[a-z0-9]/.test(previous) && /[A-Z]/.test(current));
|
||||
}
|
||||
Reference in new issue
Block a user