From d034ce71c3ea7bbaef9be7c448a0fbc71f36c4dd Mon Sep 17 00:00:00 2001 From: Donnie Date: Thu, 8 Apr 2021 14:58:02 -0700 Subject: [PATCH] Add highlighting to quick bar item text --- src/common/string/casing.ts | 5 + src/common/string/filter/sequence-matching.ts | 51 +++++++++- src/dialogs/quick-bar/ha-quick-bar.ts | 93 ++++++++++++++----- 3 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 src/common/string/casing.ts diff --git a/src/common/string/casing.ts b/src/common/string/casing.ts new file mode 100644 index 0000000000..e9bbbe7c64 --- /dev/null +++ b/src/common/string/casing.ts @@ -0,0 +1,5 @@ +export const toTitleCase = (str: string) => { + return str.replace(/\w\S*/g, (txt) => { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); +}; diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts index 8c50326fd2..297de8854a 100644 --- a/src/common/string/filter/sequence-matching.ts +++ b/src/common/string/filter/sequence-matching.ts @@ -1,4 +1,6 @@ -import { fuzzyScore } from "./filter"; +import { html, TemplateResult } from "lit-html"; +import { createMatches, FuzzyScore, fuzzyScore } from "./filter"; +import { unsafeHTML } from "lit-html/directives/unsafe-html"; /** * Determine whether a sequence of letters exists in another string, @@ -15,6 +17,7 @@ export const fuzzySequentialMatch = ( item: ScorableTextItem ) => { let topScore = Number.NEGATIVE_INFINITY; + const decoratedWords: TemplateResult[][] = []; for (const word of item.strings) { const scores = fuzzyScore( @@ -27,6 +30,8 @@ export const fuzzySequentialMatch = ( true ); + decoratedWords.push(decorateMatch(word, scores)); + if (!scores) { continue; } @@ -45,7 +50,11 @@ export const fuzzySequentialMatch = ( return undefined; } - return topScore; + return { + score: topScore, + strings: item.strings, + decoratedWords, + }; }; /** @@ -64,6 +73,7 @@ export const fuzzySequentialMatch = ( export interface ScorableTextItem { score?: number; strings: string[]; + decoratedWords?: TemplateResult[][]; } type FuzzyFilterSort = ( @@ -74,7 +84,11 @@ type FuzzyFilterSort = ( export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { return items .map((item) => { - item.score = fuzzySequentialMatch(filter, item); + const match = fuzzySequentialMatch(filter, item); + + item.score = match?.score; + item.decoratedWords = match?.decoratedWords; + return item; }) .filter((item) => item.score !== undefined) @@ -82,3 +96,34 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 ); }; + +type MatchDecorator = (word: string, scores?: FuzzyScore) => TemplateResult[]; +export const decorateMatch: MatchDecorator = (word, scores) => { + if (!scores) { + return [html`${word}`]; + } + + const decoratedText: TemplateResult[] = []; + const matches = createMatches(scores); + let pos = 0; + + let actualWord = ""; + for (const match of matches) { + actualWord += word.substring(pos, match.start); + actualWord += `${word.substring( + match.start, + match.end + )}`; + pos = match.end; + } + actualWord += word.substring(pos); + + const fragments = actualWord.split("::"); + + for (let i = 0; i < fragments.length; i++) { + const fragment = fragments[i]; + decoratedText.push(html`${unsafeHTML(fragment)}`); + } + + return decoratedText; +}; diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 1a3d49d467..75f79a0947 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -53,6 +53,7 @@ import { } from "../generic/show-dialog-box"; import { QuickBarParams } from "./show-dialog-quick-bar"; import "../../components/ha-chip"; +import { toTitleCase } from "../../common/string/casing"; interface QuickBarItem extends ScorableTextItem { primaryText: string; @@ -257,11 +258,19 @@ export class QuickBar extends LitElement { class="entity" slot="graphic" >`} - ${item.primaryText} + ${item.decoratedWords + ? item.decoratedWords[0] + : item.primaryText} ${item.altText ? html` - ${item.altText} + ${item.decoratedWords + ? item.decoratedWords[1] + : item.altText} ` : null} @@ -270,6 +279,8 @@ export class QuickBar extends LitElement { } private _renderCommandItem(item: CommandItem, index?: number) { + const decoratedItem = item.decoratedWords && item.decoratedWords[0]; + return html` ` : ""} - ${item.categoryText} - ${item.primaryText} + ${decoratedItem ? decoratedItem[1] : item.primaryText} `; } @@ -347,6 +360,10 @@ export class QuickBar extends LitElement { } else { this._commandMode = false; this._search = newFilter; + this._filter = this._search; + if (this._filter === "") { + this._clearSearch(); + } } if (oldCommandMode !== this._commandMode) { @@ -361,6 +378,18 @@ export class QuickBar extends LitElement { private _clearSearch() { this._search = ""; this._filter = ""; + this._resetDecorations(); + } + + private _resetDecorations() { + this._entityItems = this._entityItems?.map((item) => ({ + ...item, + decoratedWords: undefined, + })); + this._commandItems = this._commandItems?.map((item) => ({ + ...item, + decoratedWords: undefined, + })); } private _debouncedSetFilter = debounce((filter: string) => { @@ -425,19 +454,20 @@ export class QuickBar extends LitElement { return reloadableDomains.map((domain) => { const commandItem = { - primaryText: + primaryText: toTitleCase( this.hass.localize( `ui.dialogs.quick-bar.commands.reload.${domain}` ) || - this.hass.localize( - "ui.dialogs.quick-bar.commands.reload.reload", - "domain", - domainToName(this.hass.localize, domain) - ), + this.hass.localize( + "ui.dialogs.quick-bar.commands.reload.reload", + "domain", + domainToName(this.hass.localize, domain) + ) + ), action: () => this.hass.callService(domain, "reload"), iconPath: mdiReload, - categoryText: this.hass.localize( - `ui.dialogs.quick-bar.commands.types.reload` + categoryText: toTitleCase( + this.hass.localize(`ui.dialogs.quick-bar.commands.types.reload`) ), }; @@ -456,16 +486,20 @@ export class QuickBar extends LitElement { const categoryKey: CommandItem["categoryKey"] = "server_control"; const item = { - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.server_control.perform_action", - "action", + primaryText: toTitleCase( this.hass.localize( - `ui.dialogs.quick-bar.commands.server_control.${action}` + "ui.dialogs.quick-bar.commands.server_control.perform_action", + "action", + this.hass.localize( + `ui.dialogs.quick-bar.commands.server_control.${action}` + ) ) ), iconPath: mdiServerNetwork, - categoryText: this.hass.localize( - `ui.dialogs.quick-bar.commands.types.${categoryKey}` + categoryText: toTitleCase( + this.hass.localize( + `ui.dialogs.quick-bar.commands.types.${categoryKey}` + ) ), categoryKey, action: () => this.hass.callService("homeassistant", action), @@ -563,9 +597,12 @@ export class QuickBar extends LitElement { const navItem = { ...item, + primaryText: toTitleCase(item.primaryText), iconPath: mdiEarth, - categoryText: this.hass.localize( - `ui.dialogs.quick-bar.commands.types.${categoryKey}` + categoryText: toTitleCase( + this.hass.localize( + `ui.dialogs.quick-bar.commands.types.${categoryKey}` + ) ), action: () => navigate(this, item.path), }; @@ -647,6 +684,16 @@ export class QuickBar extends LitElement { margin-left: 8px; } + ha-chip.command-category span.highlight-letter { + font-weight: bold; + color: #0051ff; + } + + span.command-text span.highlight-letter { + font-weight: bold; + color: #0098ff; + } + .uni-virtualizer-host { display: block; position: relative; @@ -662,10 +709,6 @@ export class QuickBar extends LitElement { mwc-list-item { width: 100%; } - - mwc-list-item.command-item { - text-transform: capitalize; - } `, ]; }