mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-14 21:40:27 +00:00
Compare commits
5 Commits
copilot/fi
...
ha-icon-pi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c2d47ee3 | ||
|
|
01ba7571f0 | ||
|
|
70bb29e242 | ||
|
|
34443a009d | ||
|
|
e1dce358c3 |
@@ -103,6 +103,10 @@ export class HaGenericPicker extends LitElement {
|
|||||||
// helper to set new value after closing picker, to avoid flicker
|
// helper to set new value after closing picker, to avoid flicker
|
||||||
private _newValue?: string;
|
private _newValue?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||||
|
|
||||||
private _unsubscribeTinyKeys?: () => void;
|
private _unsubscribeTinyKeys?: () => void;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@@ -137,6 +141,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
|
.errorMessage=${this.errorMessage}
|
||||||
|
.invalid=${this.invalid}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.valueRenderer=${this.valueRenderer}
|
.valueRenderer=${this.valueRenderer}
|
||||||
>
|
>
|
||||||
@@ -205,11 +211,16 @@ export class HaGenericPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderHelper() {
|
private _renderHelper() {
|
||||||
return this.helper
|
const showError = this.invalid && this.errorMessage;
|
||||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
const showHelper = !showError && this.helper;
|
||||||
>${this.helper}</ha-input-helper-text
|
|
||||||
>`
|
if (!showError && !showHelper) {
|
||||||
: nothing;
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<ha-input-helper-text .disabled=${this.disabled}>
|
||||||
|
${showError ? this.errorMessage : this.helper}
|
||||||
|
</ha-input-helper-text>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogOpened = () => {
|
private _dialogOpened = () => {
|
||||||
@@ -308,6 +319,9 @@ export class HaGenericPicker extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
margin: var(--ha-space-2) 0 0;
|
margin: var(--ha-space-2) 0 0;
|
||||||
}
|
}
|
||||||
|
:host([invalid]) ha-input-helper-text {
|
||||||
|
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||||
|
}
|
||||||
|
|
||||||
wa-popover {
|
wa-popover {
|
||||||
--wa-space-l: var(--ha-space-0);
|
--wa-space-l: var(--ha-space-0);
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import type {
|
|
||||||
ComboBoxDataProviderCallback,
|
|
||||||
ComboBoxDataProviderParams,
|
|
||||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
@@ -10,9 +6,10 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { customIcons } from "../data/custom_icons";
|
import { customIcons } from "../data/custom_icons";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import "./ha-combo-box";
|
|
||||||
import "./ha-icon";
|
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-generic-picker";
|
||||||
|
import "./ha-icon";
|
||||||
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
|
|
||||||
interface IconItem {
|
interface IconItem {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -21,7 +18,7 @@ interface IconItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RankedIcon {
|
interface RankedIcon {
|
||||||
icon: string;
|
item: PickerComboBoxItem;
|
||||||
rank: number;
|
rank: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +64,18 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
|
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||||
<ha-combo-box-item type="button">
|
<ha-combo-box-item type="button">
|
||||||
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
<ha-icon .icon=${item.id} slot="start"></ha-icon>
|
||||||
${item.icon}
|
${item.id}
|
||||||
</ha-combo-box-item>
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const valueRenderer = (value: string) => html`
|
||||||
|
<ha-icon .icon=${value} slot="start"></ha-icon>
|
||||||
|
<span slot="headline">${value}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
@customElement("ha-icon-picker")
|
@customElement("ha-icon-picker")
|
||||||
export class HaIconPicker extends LitElement {
|
export class HaIconPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
@@ -96,13 +98,11 @@ export class HaIconPicker extends LitElement {
|
|||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
item-value-path="icon"
|
|
||||||
item-label-path="icon"
|
|
||||||
.value=${this._value}
|
.value=${this._value}
|
||||||
allow-custom-value
|
allow-custom-value
|
||||||
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
|
.getItems=${this._getItems}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@@ -110,69 +110,85 @@ export class HaIconPicker extends LitElement {
|
|||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
.errorMessage=${this.errorMessage}
|
.errorMessage=${this.errorMessage}
|
||||||
.invalid=${this.invalid}
|
.invalid=${this.invalid}
|
||||||
.renderer=${rowRenderer}
|
.rowRenderer=${rowRenderer}
|
||||||
icon
|
.valueRenderer=${valueRenderer}
|
||||||
@opened-changed=${this._openedChanged}
|
.searchFn=${this._filterIcons}
|
||||||
|
.notFoundLabel=${this.hass?.localize(
|
||||||
|
"ui.components.icon-picker.no_match"
|
||||||
|
)}
|
||||||
|
popover-placement="bottom-start"
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
${this._value || this.placeholder
|
</ha-generic-picker>
|
||||||
? html`
|
|
||||||
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
|
||||||
</ha-icon>
|
|
||||||
`
|
|
||||||
: html`<slot slot="icon" name="fallback"></slot>`}
|
|
||||||
</ha-combo-box>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||||
private _filterIcons = memoizeOne(
|
private _filterIcons = memoizeOne(
|
||||||
(filter: string, iconItems: IconItem[] = ICONS) => {
|
(filter: string, items: PickerComboBoxItem[]): PickerComboBoxItem[] => {
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
return iconItems;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredItems: RankedIcon[] = [];
|
const filteredItems: RankedIcon[] = [];
|
||||||
const addIcon = (icon: string, rank: number) =>
|
const addIcon = (item: PickerComboBoxItem, rank: number) =>
|
||||||
filteredItems.push({ icon, rank });
|
filteredItems.push({ item, rank });
|
||||||
|
|
||||||
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
||||||
for (const item of iconItems) {
|
for (const item of items) {
|
||||||
if (item.parts.has(filter)) {
|
const iconName = item.id.split(":")[1] || item.id;
|
||||||
addIcon(item.icon, 1);
|
const parts = iconName.split("-");
|
||||||
} else if (item.keywords.includes(filter)) {
|
const keywords = item.search_labels?.slice(1) || [];
|
||||||
addIcon(item.icon, 2);
|
|
||||||
} else if (item.icon.includes(filter)) {
|
if (parts.includes(filter)) {
|
||||||
addIcon(item.icon, 3);
|
addIcon(item, 1);
|
||||||
} else if (item.keywords.some((word) => word.includes(filter))) {
|
} else if (keywords.includes(filter)) {
|
||||||
addIcon(item.icon, 4);
|
addIcon(item, 2);
|
||||||
|
} else if (item.id.includes(filter)) {
|
||||||
|
addIcon(item, 3);
|
||||||
|
} else if (keywords.some((word) => word.includes(filter))) {
|
||||||
|
addIcon(item, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow preview for custom icon not in list
|
// Allow preview for custom icon not in list
|
||||||
if (filteredItems.length === 0) {
|
if (filteredItems.length === 0) {
|
||||||
addIcon(filter, 0);
|
addIcon(
|
||||||
|
{
|
||||||
|
id: filter,
|
||||||
|
primary: filter,
|
||||||
|
icon: filter,
|
||||||
|
search_labels: [filter],
|
||||||
|
sorting_label: filter,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
|
return filteredItems
|
||||||
|
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
||||||
|
.map((item) => item.item);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _iconProvider = (
|
private _getItems = (): PickerComboBoxItem[] =>
|
||||||
params: ComboBoxDataProviderParams,
|
ICONS.map((icon: IconItem) => ({
|
||||||
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
|
id: icon.icon,
|
||||||
) => {
|
primary: icon.icon,
|
||||||
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
|
icon: icon.icon,
|
||||||
const iStart = params.page * params.pageSize;
|
search_labels: [
|
||||||
const iEnd = iStart + params.pageSize;
|
icon.icon.split(":")[1] || icon.icon,
|
||||||
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
|
...Array.from(icon.parts),
|
||||||
};
|
...icon.keywords,
|
||||||
|
],
|
||||||
|
sorting_label: icon.icon,
|
||||||
|
}));
|
||||||
|
|
||||||
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
|
protected firstUpdated() {
|
||||||
const opened = ev.detail.value;
|
if (!ICONS_LOADED) {
|
||||||
if (opened && !ICONS_LOADED) {
|
loadIcons().then(() => {
|
||||||
await loadIcons();
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,15 +215,9 @@ export class HaIconPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
*[slot="icon"] {
|
ha-generic-picker {
|
||||||
color: var(--primary-text-color);
|
width: 100%;
|
||||||
position: relative;
|
display: block;
|
||||||
bottom: 2px;
|
|
||||||
}
|
|
||||||
*[slot="prefix"] {
|
|
||||||
margin-right: 8px;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
margin-inline-start: initial;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export class HaPickerField extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public valueRenderer?: PickerValueRenderer;
|
public valueRenderer?: PickerValueRenderer;
|
||||||
|
|
||||||
|
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||||
|
|
||||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||||
|
|
||||||
public async focus() {
|
public async focus() {
|
||||||
@@ -142,6 +146,11 @@ export class HaPickerField extends LitElement {
|
|||||||
background-color: var(--mdc-theme-primary);
|
background-color: var(--mdc-theme-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([invalid]) ha-combo-box-item:after {
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||||
|
}
|
||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
margin: 0 -8px;
|
margin: 0 -8px;
|
||||||
--mdc-icon-button-size: 32px;
|
--mdc-icon-button-size: 32px;
|
||||||
|
|||||||
@@ -761,6 +761,9 @@
|
|||||||
"no_match": "No matching languages found",
|
"no_match": "No matching languages found",
|
||||||
"no_languages": "No languages available"
|
"no_languages": "No languages available"
|
||||||
},
|
},
|
||||||
|
"icon-picker": {
|
||||||
|
"no_match": "No matching icons found"
|
||||||
|
},
|
||||||
"tts-picker": {
|
"tts-picker": {
|
||||||
"tts": "Text-to-speech",
|
"tts": "Text-to-speech",
|
||||||
"none": "None"
|
"none": "None"
|
||||||
|
|||||||
Reference in New Issue
Block a user