mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-03 14:37:47 +00:00
Add highlighting to quick bar item text
This commit is contained in:
parent
cfbfdda011
commit
d034ce71c3
5
src/common/string/casing.ts
Normal file
5
src/common/string/casing.ts
Normal 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();
|
||||||
|
});
|
||||||
|
};
|
@ -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,
|
* Determine whether a sequence of letters exists in another string,
|
||||||
@ -15,6 +17,7 @@ export const fuzzySequentialMatch = (
|
|||||||
item: ScorableTextItem
|
item: ScorableTextItem
|
||||||
) => {
|
) => {
|
||||||
let topScore = Number.NEGATIVE_INFINITY;
|
let topScore = Number.NEGATIVE_INFINITY;
|
||||||
|
const decoratedWords: TemplateResult[][] = [];
|
||||||
|
|
||||||
for (const word of item.strings) {
|
for (const word of item.strings) {
|
||||||
const scores = fuzzyScore(
|
const scores = fuzzyScore(
|
||||||
@ -27,6 +30,8 @@ export const fuzzySequentialMatch = (
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
decoratedWords.push(decorateMatch(word, scores));
|
||||||
|
|
||||||
if (!scores) {
|
if (!scores) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -45,7 +50,11 @@ export const fuzzySequentialMatch = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return topScore;
|
return {
|
||||||
|
score: topScore,
|
||||||
|
strings: item.strings,
|
||||||
|
decoratedWords,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,6 +73,7 @@ export const fuzzySequentialMatch = (
|
|||||||
export interface ScorableTextItem {
|
export interface ScorableTextItem {
|
||||||
score?: number;
|
score?: number;
|
||||||
strings: string[];
|
strings: string[];
|
||||||
|
decoratedWords?: TemplateResult[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||||
@ -74,7 +84,11 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
|
|||||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||||
return items
|
return items
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
item.score = fuzzySequentialMatch(filter, item);
|
const match = fuzzySequentialMatch(filter, item);
|
||||||
|
|
||||||
|
item.score = match?.score;
|
||||||
|
item.decoratedWords = match?.decoratedWords;
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
.filter((item) => item.score !== undefined)
|
.filter((item) => item.score !== undefined)
|
||||||
@ -82,3 +96,34 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
|||||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
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;
|
||||||
|
};
|
||||||
|
@ -53,6 +53,7 @@ import {
|
|||||||
} from "../generic/show-dialog-box";
|
} from "../generic/show-dialog-box";
|
||||||
import { QuickBarParams } from "./show-dialog-quick-bar";
|
import { QuickBarParams } from "./show-dialog-quick-bar";
|
||||||
import "../../components/ha-chip";
|
import "../../components/ha-chip";
|
||||||
|
import { toTitleCase } from "../../common/string/casing";
|
||||||
|
|
||||||
interface QuickBarItem extends ScorableTextItem {
|
interface QuickBarItem extends ScorableTextItem {
|
||||||
primaryText: string;
|
primaryText: string;
|
||||||
@ -257,11 +258,19 @@ export class QuickBar extends LitElement {
|
|||||||
class="entity"
|
class="entity"
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
></ha-icon>`}
|
></ha-icon>`}
|
||||||
<span>${item.primaryText}</span>
|
<span
|
||||||
|
>${item.decoratedWords
|
||||||
|
? item.decoratedWords[0]
|
||||||
|
: item.primaryText}</span
|
||||||
|
>
|
||||||
${item.altText
|
${item.altText
|
||||||
? html`
|
? html`
|
||||||
<span slot="secondary" class="item-text secondary"
|
<span slot="secondary" class="item-text secondary">
|
||||||
>${item.altText}</span
|
<span
|
||||||
|
>${item.decoratedWords
|
||||||
|
? item.decoratedWords[1]
|
||||||
|
: item.altText}</span
|
||||||
|
></span
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
: null}
|
: null}
|
||||||
@ -270,6 +279,8 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
private _renderCommandItem(item: CommandItem, index?: number) {
|
||||||
|
const decoratedItem = item.decoratedWords && item.decoratedWords[0];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
.item=${item}
|
.item=${item}
|
||||||
@ -289,11 +300,13 @@ export class QuickBar extends LitElement {
|
|||||||
slot="icon"
|
slot="icon"
|
||||||
></ha-svg-icon>`
|
></ha-svg-icon>`
|
||||||
: ""}
|
: ""}
|
||||||
${item.categoryText}</ha-chip
|
${decoratedItem ? decoratedItem[0] : item.categoryText}</ha-chip
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="command-text">${item.primaryText}</span>
|
<span class="command-text"
|
||||||
|
>${decoratedItem ? decoratedItem[1] : item.primaryText}</span
|
||||||
|
>
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -347,6 +360,10 @@ export class QuickBar extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
this._commandMode = false;
|
this._commandMode = false;
|
||||||
this._search = newFilter;
|
this._search = newFilter;
|
||||||
|
this._filter = this._search;
|
||||||
|
if (this._filter === "") {
|
||||||
|
this._clearSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldCommandMode !== this._commandMode) {
|
if (oldCommandMode !== this._commandMode) {
|
||||||
@ -361,6 +378,18 @@ export class QuickBar extends LitElement {
|
|||||||
private _clearSearch() {
|
private _clearSearch() {
|
||||||
this._search = "";
|
this._search = "";
|
||||||
this._filter = "";
|
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) => {
|
private _debouncedSetFilter = debounce((filter: string) => {
|
||||||
@ -425,19 +454,20 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
return reloadableDomains.map((domain) => {
|
return reloadableDomains.map((domain) => {
|
||||||
const commandItem = {
|
const commandItem = {
|
||||||
primaryText:
|
primaryText: toTitleCase(
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`ui.dialogs.quick-bar.commands.reload.${domain}`
|
`ui.dialogs.quick-bar.commands.reload.${domain}`
|
||||||
) ||
|
) ||
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
"ui.dialogs.quick-bar.commands.reload.reload",
|
"ui.dialogs.quick-bar.commands.reload.reload",
|
||||||
"domain",
|
"domain",
|
||||||
domainToName(this.hass.localize, domain)
|
domainToName(this.hass.localize, domain)
|
||||||
),
|
)
|
||||||
|
),
|
||||||
action: () => this.hass.callService(domain, "reload"),
|
action: () => this.hass.callService(domain, "reload"),
|
||||||
iconPath: mdiReload,
|
iconPath: mdiReload,
|
||||||
categoryText: this.hass.localize(
|
categoryText: toTitleCase(
|
||||||
`ui.dialogs.quick-bar.commands.types.reload`
|
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 categoryKey: CommandItem["categoryKey"] = "server_control";
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
primaryText: this.hass.localize(
|
primaryText: toTitleCase(
|
||||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
|
||||||
"action",
|
|
||||||
this.hass.localize(
|
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,
|
iconPath: mdiServerNetwork,
|
||||||
categoryText: this.hass.localize(
|
categoryText: toTitleCase(
|
||||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
this.hass.localize(
|
||||||
|
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||||
|
)
|
||||||
),
|
),
|
||||||
categoryKey,
|
categoryKey,
|
||||||
action: () => this.hass.callService("homeassistant", action),
|
action: () => this.hass.callService("homeassistant", action),
|
||||||
@ -563,9 +597,12 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
const navItem = {
|
const navItem = {
|
||||||
...item,
|
...item,
|
||||||
|
primaryText: toTitleCase(item.primaryText),
|
||||||
iconPath: mdiEarth,
|
iconPath: mdiEarth,
|
||||||
categoryText: this.hass.localize(
|
categoryText: toTitleCase(
|
||||||
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
this.hass.localize(
|
||||||
|
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
|
||||||
|
)
|
||||||
),
|
),
|
||||||
action: () => navigate(this, item.path),
|
action: () => navigate(this, item.path),
|
||||||
};
|
};
|
||||||
@ -647,6 +684,16 @@ export class QuickBar extends LitElement {
|
|||||||
margin-left: 8px;
|
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 {
|
.uni-virtualizer-host {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -662,10 +709,6 @@ export class QuickBar extends LitElement {
|
|||||||
mwc-list-item {
|
mwc-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-list-item.command-item {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user