-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add state content component (#21370)
* Move state content into its own component for reusability * Add entity state content selector * Use live timer * Rename live timer to remaining time and remove remaining attribute from state content list * Move default in state content component * Fix picker
- Loading branch information
Showing
11 changed files
with
585 additions
and
266 deletions.
There are no files selected for viewing
314 changes: 314 additions & 0 deletions
314
src/components/entity/ha-entity-state-content-picker.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,314 @@ | ||
import { mdiDrag } from "@mdi/js"; | ||
import { HassEntity } from "home-assistant-js-websocket"; | ||
import { LitElement, PropertyValues, css, html, nothing } from "lit"; | ||
import { customElement, property, query, state } from "lit/decorators"; | ||
import { repeat } from "lit/directives/repeat"; | ||
import memoizeOne from "memoize-one"; | ||
import { ensureArray } from "../../common/array/ensure-array"; | ||
import { fireEvent } from "../../common/dom/fire_event"; | ||
import { computeDomain } from "../../common/entity/compute_domain"; | ||
import { | ||
STATE_DISPLAY_SPECIAL_CONTENT, | ||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS, | ||
} from "../../state-display/state-display"; | ||
import { HomeAssistant, ValueChangedEvent } from "../../types"; | ||
import "../ha-combo-box"; | ||
import type { HaComboBox } from "../ha-combo-box"; | ||
|
||
const HIDDEN_ATTRIBUTES = [ | ||
"access_token", | ||
"available_modes", | ||
"battery_icon", | ||
"battery_level", | ||
"code_arm_required", | ||
"code_format", | ||
"color_modes", | ||
"device_class", | ||
"editable", | ||
"effect_list", | ||
"entity_id", | ||
"entity_picture", | ||
"event_types", | ||
"fan_modes", | ||
"fan_speed_list", | ||
"friendly_name", | ||
"frontend_stream_type", | ||
"has_date", | ||
"has_time", | ||
"hvac_modes", | ||
"icon", | ||
"id", | ||
"max_color_temp_kelvin", | ||
"max_mireds", | ||
"max_temp", | ||
"max", | ||
"min_color_temp_kelvin", | ||
"min_mireds", | ||
"min_temp", | ||
"min", | ||
"mode", | ||
"operation_list", | ||
"options", | ||
"percentage_step", | ||
"precipitation_unit", | ||
"preset_modes", | ||
"pressure_unit", | ||
"remaining", | ||
"sound_mode_list", | ||
"source_list", | ||
"state_class", | ||
"step", | ||
"supported_color_modes", | ||
"supported_features", | ||
"swing_modes", | ||
"target_temp_step", | ||
"temperature_unit", | ||
"token", | ||
"unit_of_measurement", | ||
"visibility_unit", | ||
"wind_speed_unit", | ||
]; | ||
|
||
@customElement("ha-entity-state-content-picker") | ||
class HaEntityStatePicker extends LitElement { | ||
@property({ attribute: false }) public hass!: HomeAssistant; | ||
|
||
@property({ attribute: false }) public entityId?: string; | ||
|
||
@property({ type: Boolean }) public autofocus = false; | ||
|
||
@property({ type: Boolean }) public disabled = false; | ||
|
||
@property({ type: Boolean }) public required = false; | ||
|
||
@property() public label?: string; | ||
|
||
@property() public value?: string[] | string; | ||
|
||
@property() public helper?: string; | ||
|
||
@state() private _opened = false; | ||
|
||
@query("ha-combo-box", true) private _comboBox!: HaComboBox; | ||
|
||
protected shouldUpdate(changedProps: PropertyValues) { | ||
return !(!changedProps.has("_opened") && this._opened); | ||
} | ||
|
||
private options = memoizeOne((entityId?: string, stateObj?: HassEntity) => { | ||
const domain = entityId ? computeDomain(entityId) : undefined; | ||
return [ | ||
{ | ||
label: this.hass.localize("ui.components.state-content-picker.state"), | ||
value: "state", | ||
}, | ||
{ | ||
label: this.hass.localize( | ||
"ui.components.state-content-picker.last_changed" | ||
), | ||
value: "last_changed", | ||
}, | ||
{ | ||
label: this.hass.localize( | ||
"ui.components.state-content-picker.last_updated" | ||
), | ||
value: "last_updated", | ||
}, | ||
...(domain | ||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) => | ||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content) | ||
).map((content) => ({ | ||
label: this.hass.localize( | ||
`ui.components.state-content-picker.${content}` | ||
), | ||
value: content, | ||
})) | ||
: []), | ||
...Object.keys(stateObj?.attributes ?? {}) | ||
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a)) | ||
.map((attribute) => ({ | ||
value: attribute, | ||
label: this.hass.formatEntityAttributeName(stateObj!, attribute), | ||
})), | ||
]; | ||
}); | ||
|
||
private _filter = ""; | ||
|
||
protected render() { | ||
if (!this.hass) { | ||
return nothing; | ||
} | ||
|
||
const value = this._value; | ||
|
||
const stateObj = this.entityId | ||
? this.hass.states[this.entityId] | ||
: undefined; | ||
|
||
const options = this.options(this.entityId, stateObj); | ||
const optionItems = options.filter( | ||
(option) => !this._value.includes(option.value) | ||
); | ||
|
||
return html` | ||
${value?.length | ||
? html` | ||
<ha-sortable | ||
no-style | ||
@item-moved=${this._moveItem} | ||
.disabled=${this.disabled} | ||
> | ||
<ha-chip-set> | ||
${repeat( | ||
this._value, | ||
(item) => item, | ||
(item, idx) => { | ||
const label = | ||
options.find((option) => option.value === item)?.label || | ||
item; | ||
return html` | ||
<ha-input-chip | ||
.idx=${idx} | ||
@remove=${this._removeItem} | ||
.label=${label} | ||
selected | ||
> | ||
<ha-svg-icon | ||
slot="icon" | ||
.path=${mdiDrag} | ||
data-handle | ||
></ha-svg-icon> | ||
${label} | ||
</ha-input-chip> | ||
`; | ||
} | ||
)} | ||
</ha-chip-set> | ||
</ha-sortable> | ||
` | ||
: nothing} | ||
<ha-combo-box | ||
item-value-path="value" | ||
item-label-path="label" | ||
.hass=${this.hass} | ||
.label=${this.label} | ||
.helper=${this.helper} | ||
.disabled=${this.disabled} | ||
.required=${this.required && !value.length} | ||
.value=${""} | ||
.items=${optionItems} | ||
allow-custom-value | ||
@filter-changed=${this._filterChanged} | ||
@value-changed=${this._comboBoxValueChanged} | ||
@opened-changed=${this._openedChanged} | ||
></ha-combo-box> | ||
`; | ||
} | ||
|
||
private get _value() { | ||
return !this.value ? [] : ensureArray(this.value); | ||
} | ||
|
||
private _openedChanged(ev: ValueChangedEvent<boolean>) { | ||
this._opened = ev.detail.value; | ||
} | ||
|
||
private _filterChanged(ev?: CustomEvent): void { | ||
this._filter = ev?.detail.value || ""; | ||
|
||
const filteredItems = this._comboBox.items?.filter((item) => { | ||
const label = item.label || item.value; | ||
return label.toLowerCase().includes(this._filter?.toLowerCase()); | ||
}); | ||
|
||
if (this._filter) { | ||
filteredItems?.unshift({ label: this._filter, value: this._filter }); | ||
} | ||
|
||
this._comboBox.filteredItems = filteredItems; | ||
} | ||
|
||
private async _moveItem(ev: CustomEvent) { | ||
ev.stopPropagation(); | ||
const { oldIndex, newIndex } = ev.detail; | ||
const value = this._value; | ||
const newValue = value.concat(); | ||
const element = newValue.splice(oldIndex, 1)[0]; | ||
newValue.splice(newIndex, 0, element); | ||
this._setValue(newValue); | ||
await this.updateComplete; | ||
this._filterChanged(); | ||
} | ||
|
||
private async _removeItem(ev) { | ||
ev.stopPropagation(); | ||
const value: string[] = [...this._value]; | ||
value.splice(ev.target.idx, 1); | ||
this._setValue(value); | ||
await this.updateComplete; | ||
this._filterChanged(); | ||
} | ||
|
||
private _comboBoxValueChanged(ev: CustomEvent): void { | ||
ev.stopPropagation(); | ||
const newValue = ev.detail.value; | ||
|
||
if (this.disabled || newValue === "") { | ||
return; | ||
} | ||
|
||
const currentValue = this._value; | ||
|
||
if (currentValue.includes(newValue)) { | ||
return; | ||
} | ||
|
||
setTimeout(() => { | ||
this._filterChanged(); | ||
this._comboBox.setInputValue(""); | ||
}, 0); | ||
|
||
this._setValue([...currentValue, newValue]); | ||
} | ||
|
||
private _setValue(value: string[]) { | ||
const newValue = | ||
value.length === 0 ? undefined : value.length === 1 ? value[0] : value; | ||
this.value = newValue; | ||
fireEvent(this, "value-changed", { | ||
value: newValue, | ||
}); | ||
} | ||
|
||
static styles = css` | ||
:host { | ||
position: relative; | ||
} | ||
ha-chip-set { | ||
padding: 8px 0; | ||
} | ||
.sortable-fallback { | ||
display: none; | ||
opacity: 0; | ||
} | ||
.sortable-ghost { | ||
opacity: 0.4; | ||
} | ||
.sortable-drag { | ||
cursor: grabbing; | ||
} | ||
`; | ||
} | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"ha-entity-state-content-picker": HaEntityStatePicker; | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/components/ha-selector/ha-selector-ui-state-content.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { html, LitElement } from "lit"; | ||
import { customElement, property } from "lit/decorators"; | ||
import { UiStateContentSelector } from "../../data/selector"; | ||
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||
import { HomeAssistant } from "../../types"; | ||
import "../entity/ha-entity-state-content-picker"; | ||
|
||
@customElement("ha-selector-ui_state_content") | ||
export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) { | ||
@property({ attribute: false }) public hass!: HomeAssistant; | ||
|
||
@property({ attribute: false }) public selector!: UiStateContentSelector; | ||
|
||
@property() public value?: string | string[]; | ||
|
||
@property() public label?: string; | ||
|
||
@property() public helper?: string; | ||
|
||
@property({ type: Boolean }) public disabled = false; | ||
|
||
@property({ type: Boolean }) public required = true; | ||
|
||
@property({ attribute: false }) public context?: { | ||
filter_entity?: string; | ||
}; | ||
|
||
protected render() { | ||
return html` | ||
<ha-entity-state-content-picker | ||
.hass=${this.hass} | ||
.entityId=${this.selector.ui_state_content?.entity_id || | ||
this.context?.filter_entity} | ||
.value=${this.value} | ||
.label=${this.label} | ||
.helper=${this.helper} | ||
.disabled=${this.disabled} | ||
.required=${this.required} | ||
></ha-entity-state-content-picker> | ||
`; | ||
} | ||
} | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
"ha-selector-ui_state_content": HaSelectorUiStateContent; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.