Add highlighting to quick bar item text

This commit is contained in:
Donnie 2021-04-08 14:58:02 -07:00
parent cfbfdda011
commit d034ce71c3
3 changed files with 121 additions and 28 deletions

View File

@ -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();
});
};

View File

@ -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 = <T extends ScorableTextItem>(
@ -74,7 +84,11 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
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 += `<span class="highlight-letter">${word.substring(
match.start,
match.end
)}</span>`;
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;
};

View File

@ -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"
></ha-icon>`}
<span>${item.primaryText}</span>
<span
>${item.decoratedWords
? item.decoratedWords[0]
: item.primaryText}</span
>
${item.altText
? html`
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
<span slot="secondary" class="item-text secondary">
<span
>${item.decoratedWords
? item.decoratedWords[1]
: item.altText}</span
></span
>
`
: 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`
<mwc-list-item
.item=${item}
@ -289,11 +300,13 @@ export class QuickBar extends LitElement {
slot="icon"
></ha-svg-icon>`
: ""}
${item.categoryText}</ha-chip
${decoratedItem ? decoratedItem[0] : item.categoryText}</ha-chip
>
</span>
<span class="command-text">${item.primaryText}</span>
<span class="command-text"
>${decoratedItem ? decoratedItem[1] : item.primaryText}</span
>
</mwc-list-item>
`;
}
@ -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;
}
`,
];
}