-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
400 additions
and
139 deletions.
There are no files selected for viewing
313 changes: 313 additions & 0 deletions
313
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,313 @@ | ||
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", | ||
"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", | ||
"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", | ||
"battery_icon", | ||
"battery_level", | ||
]; | ||
|
||
@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[content]?.includes(domain) | ||
).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} | ||
.allowCustomValue | ||
@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.