From 945e3610526b4f415983cb3efbdafc446339ba42 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 24 Jun 2024 14:16:40 +0200 Subject: [PATCH] Ignore diacritics in search --- hassio/src/components/hassio-filter-addons.ts | 6 ++++-- src/common/string/filter/sequence-matching.ts | 5 +++-- src/common/string/strip-diacritics.ts | 2 ++ .../data-table/sort-filter-worker.ts | 19 +++++++++---------- .../add-automation-element-dialog.ts | 4 +++- .../integrations/dialog-add-integration.ts | 9 ++++++--- .../ha-config-integrations-dashboard.ts | 6 +++++- .../editor/card-editor/hui-card-picker.ts | 4 +++- 8 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 src/common/string/strip-diacritics.ts diff --git a/hassio/src/components/hassio-filter-addons.ts b/hassio/src/components/hassio-filter-addons.ts index 76635c62dcc2..cb72ab3a52b8 100644 --- a/hassio/src/components/hassio-filter-addons.ts +++ b/hassio/src/components/hassio-filter-addons.ts @@ -1,5 +1,6 @@ -import Fuse from "fuse.js"; import type { IFuseOptions } from "fuse.js"; +import Fuse from "fuse.js"; +import { stripDiacritics } from "../../../src/common/string/strip-diacritics"; import { StoreAddon } from "../../../src/data/supervisor/store"; export function filterAndSort(addons: StoreAddon[], filter: string) { @@ -8,7 +9,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) { isCaseSensitive: false, minMatchCharLength: Math.min(filter.length, 2), threshold: 0.2, + getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)), }; const fuse = new Fuse(addons, options); - return fuse.search(filter).map((result) => result.item); + return fuse.search(stripDiacritics(filter)).map((result) => result.item); } diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts index d502cec35028..42d7aea6e9f3 100644 --- a/src/common/string/filter/sequence-matching.ts +++ b/src/common/string/filter/sequence-matching.ts @@ -1,3 +1,4 @@ +import { stripDiacritics } from "../strip-diacritics"; import { fuzzyScore } from "./filter"; /** @@ -19,10 +20,10 @@ export const fuzzySequentialMatch = ( for (const word of item.strings) { const scores = fuzzyScore( filter, - filter.toLowerCase(), + stripDiacritics(filter.toLowerCase()), 0, word, - word.toLowerCase(), + stripDiacritics(word.toLowerCase()), 0, true ); diff --git a/src/common/string/strip-diacritics.ts b/src/common/string/strip-diacritics.ts new file mode 100644 index 000000000000..a82ddf2c4521 --- /dev/null +++ b/src/common/string/strip-diacritics.ts @@ -0,0 +1,2 @@ +export const stripDiacritics = (str) => + str.normalize("NFD").replace(/[\u0300-\u036F]/g, ""); diff --git a/src/components/data-table/sort-filter-worker.ts b/src/components/data-table/sort-filter-worker.ts index 7b6d840162b4..b34ec899ff7d 100644 --- a/src/components/data-table/sort-filter-worker.ts +++ b/src/components/data-table/sort-filter-worker.ts @@ -1,5 +1,6 @@ import { expose } from "comlink"; import { stringCompare } from "../../common/string/compare"; +import { stripDiacritics } from "../../common/string/strip-diacritics"; import type { ClonedDataTableColumnData, DataTableRowData, @@ -12,20 +13,18 @@ const filterData = ( columns: SortableColumnContainer, filter: string ) => { - filter = filter.toUpperCase(); + filter = stripDiacritics(filter.toLowerCase()); return data.filter((row) => Object.entries(columns).some((columnEntry) => { const [key, column] = columnEntry; if (column.filterable) { - if ( - String( - column.filterKey - ? row[column.valueColumn || key][column.filterKey] - : row[column.valueColumn || key] - ) - .toUpperCase() - .includes(filter) - ) { + const value = String( + column.filterKey + ? row[column.valueColumn || key][column.filterKey] + : row[column.valueColumn || key] + ); + + if (stripDiacritics(value).toLowerCase().includes(filter)) { return true; } } diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 09fcf360a1ad..6f75232f52cd 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -54,6 +54,7 @@ import { AddAutomationElementDialogParams, PASTE_VALUE, } from "./show-add-automation-element-dialog"; +import { stripDiacritics } from "../../../common/string/strip-diacritics"; const TYPES = { trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS }, @@ -208,9 +209,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { isCaseSensitive: false, minMatchCharLength: Math.min(filter.length, 2), threshold: 0.2, + getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)), }; const fuse = new Fuse(items, options); - return fuse.search(filter).map((result) => result.item); + return fuse.search(stripDiacritics(filter)).map((result) => result.item); } ); diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index f03d3be6f37a..25b131f7103a 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -55,6 +55,7 @@ import { showYamlIntegrationDialog, } from "./show-add-integration-dialog"; import { getConfigEntries } from "../../../data/config_entries"; +import { stripDiacritics } from "../../../common/string/strip-diacritics"; export interface IntegrationListItem { name: string; @@ -255,6 +256,7 @@ class AddIntegrationDialog extends LitElement { isCaseSensitive: false, minMatchCharLength: Math.min(filter.length, 2), threshold: 0.2, + getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)), }; const helpers = Object.entries(h).map(([domain, integration]) => ({ domain, @@ -264,15 +266,16 @@ class AddIntegrationDialog extends LitElement { is_built_in: integration.is_built_in !== false, cloud: integration.iot_class?.startsWith("cloud_"), })); + const filterWithAccents = stripDiacritics(filter); return [ ...new Fuse(integrations, options) - .search(filter) + .search(filterWithAccents) .map((result) => result.item), ...new Fuse(yamlIntegrations, options) - .search(filter) + .search(filterWithAccents) .map((result) => result.item), ...new Fuse(helpers, options) - .search(filter) + .search(filterWithAccents) .map((result) => result.item), ]; } diff --git a/src/panels/config/integrations/ha-config-integrations-dashboard.ts b/src/panels/config/integrations/ha-config-integrations-dashboard.ts index d3dad024f27b..48987048d8f3 100644 --- a/src/panels/config/integrations/ha-config-integrations-dashboard.ts +++ b/src/panels/config/integrations/ha-config-integrations-dashboard.ts @@ -71,6 +71,7 @@ import { showAddIntegrationDialog } from "./show-add-integration-dialog"; import "./ha-disabled-config-entry-card"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import "../../../components/search-input-outlined"; +import { stripDiacritics } from "../../../common/string/strip-diacritics"; export interface ConfigEntryExtended extends ConfigEntry { localized_domain_name?: string; @@ -208,9 +209,12 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) { isCaseSensitive: false, minMatchCharLength: Math.min(filter.length, 2), threshold: 0.2, + getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)), }; const fuse = new Fuse(configEntriesInProgress, options); - filteredEntries = fuse.search(filter).map((result) => result.item); + filteredEntries = fuse + .search(stripDiacritics(filter)) + .map((result) => result.item); } else { filteredEntries = configEntriesInProgress; } diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index e92aba044dbe..65d8d94bdd2d 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -16,6 +16,7 @@ import memoizeOne from "memoize-one"; import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stringCompare } from "../../../../common/string/compare"; +import { stripDiacritics } from "../../../../common/string/strip-diacritics"; import "../../../../components/ha-circular-progress"; import "../../../../components/search-input"; import { isUnavailableState } from "../../../../data/entity"; @@ -86,9 +87,10 @@ export class HuiCardPicker extends LitElement { isCaseSensitive: false, minMatchCharLength: Math.min(filter.length, 2), threshold: 0.2, + getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)), }; const fuse = new Fuse(cards, options); - cards = fuse.search(filter).map((result) => result.item); + cards = fuse.search(stripDiacritics(filter)).map((result) => result.item); return cardElements.filter((cardElement: CardElement) => cards.includes(cardElement.card) );