Enhancement: diacritic-insensitive UI filters

This commit is contained in:
shamoon
2026-06-16 08:59:55 -07:00
parent ad1b54ce88
commit aa14079767
4 changed files with 56 additions and 6 deletions
@@ -23,6 +23,7 @@ import {
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { pngxPopperOptions } from 'src/app/utils/popper-options'
import { matchesSearchText } from 'src/app/utils/text-search'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@@ -69,9 +70,7 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
public get filteredFields(): CustomField[] {
return this.unusedFields.filter(
(f) =>
!this.filterText ||
f.name.toLowerCase().includes(this.filterText.toLowerCase())
(f) => !this.filterText || matchesSearchText(f.name, this.filterText)
)
}
+2 -3
View File
@@ -1,5 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core'
import { MatchingModel } from '../data/matching-model'
import { matchesSearchText } from '../utils/text-search'
@Pipe({
name: 'filter',
@@ -21,9 +22,7 @@ export class FilterPipe implements PipeTransform {
typeof item[key] === 'string' || typeof item[key] === 'number'
)
return keys.some((key) => {
return String(item[key])
.toLowerCase()
.includes(searchText.toLowerCase())
return matchesSearchText(item[key], searchText)
})
})
}
+10
View File
@@ -0,0 +1,10 @@
import { matchesSearchText } from './text-search'
describe('text search utilities', () => {
it('matches text accent-insensitively', () => {
expect(matchesSearchText('R\u00e9sum\u00e9', 'resume')).toBeTruthy()
expect(matchesSearchText('S\u00f8ren', 'soren')).toBeTruthy()
expect(matchesSearchText('\u0152uvre', 'oeuvre')).toBeTruthy()
expect(matchesSearchText('Invoice', 'receipt')).toBeFalsy()
})
})
+42
View File
@@ -0,0 +1,42 @@
const EXTRA_DIACRITIC_REPLACEMENTS: Record<string, string> = {
'\u00c6': 'AE',
'\u00e6': 'ae',
'\u00d0': 'D',
'\u00f0': 'd',
'\u00d8': 'O',
'\u00f8': 'o',
'\u00de': 'Th',
'\u00fe': 'th',
'\u0110': 'D',
'\u0111': 'd',
'\u0131': 'i',
'\u0141': 'L',
'\u0142': 'l',
'\u0152': 'OE',
'\u0153': 'oe',
'\u00df': 'ss',
}
// ng-select has a similar private helper stripSpecialChars, but we can't use it
const EXTRA_DIACRITICS_PATTERN = new RegExp(
`[${Object.keys(EXTRA_DIACRITIC_REPLACEMENTS).join('')}]`,
'g'
)
function normalizeSearchText(value: unknown): string {
return String(value ?? '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(
EXTRA_DIACRITICS_PATTERN,
(char) => EXTRA_DIACRITIC_REPLACEMENTS[char] ?? char
)
.toLocaleLowerCase()
}
export function matchesSearchText(
value: unknown,
searchText: unknown
): boolean {
return normalizeSearchText(value).includes(normalizeSearchText(searchText))
}