diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index b6ff6d1711..0722040cc0 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -38,6 +38,7 @@ const SCHEMAS: { select: "Select", icon: "Icon", media: "Media", + location: "Location", }, schema: [ { name: "addon", selector: { addon: {} } }, @@ -75,6 +76,10 @@ const SCHEMAS: { media: {}, }, }, + { + name: "location", + selector: { location: { radius: true, icon: "mdi:home" } }, + }, ], }, { diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index ea4a892015..600176d024 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -168,6 +168,11 @@ const SCHEMAS: { }, icon: { name: "Icon", selector: { icon: {} } }, media: { name: "Media", selector: { media: {} } }, + location: { name: "Location", selector: { location: {} } }, + location_radius: { + name: "Location with radius", + selector: { location: { radius: true, icon: "mdi:home" } }, + }, }, }, ]; diff --git a/src/components/ha-form/ha-form-constant.ts b/src/components/ha-form/ha-form-constant.ts index f20c057dfc..c1b6c67e9e 100644 --- a/src/components/ha-form/ha-form-constant.ts +++ b/src/components/ha-form/ha-form-constant.ts @@ -9,7 +9,9 @@ export class HaFormConstant extends LitElement implements HaFormElement { @property() public label!: string; protected render(): TemplateResult { - return html`${this.label}: ${this.schema.value}`; + return html`${this.label}${this.schema.value + ? `: ${this.schema.value}` + : ""}`; } static get styles(): CSSResultGroup { diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index c824d8fb89..e1e87d86f9 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -25,6 +25,8 @@ import { HomeAssistant } from "../../types"; const getValue = (obj, item) => obj ? (!item.name ? obj : obj[item.name]) : null; +const getError = (obj, item) => (obj && item.name ? obj[item.name] : null); + let selectorImported = false; @customElement("ha-form") @@ -84,7 +86,7 @@ export class HaForm extends LitElement implements HaFormElement { ` : ""} ${this.schema.map((item) => { - const error = getValue(this.error, item); + const error = getError(this.error, item); return html` ${error diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index fc45bc3ef1..2c3a9912d4 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -40,7 +40,7 @@ export interface HaFormSelector extends HaFormBaseSchema { export interface HaFormConstantSchema extends HaFormBaseSchema { type: "constant"; - value: string; + value?: string; } export interface HaFormIntegerSchema extends HaFormBaseSchema { diff --git a/src/components/ha-selector/ha-selector-location.ts b/src/components/ha-selector/ha-selector-location.ts new file mode 100644 index 0000000000..18372f525d --- /dev/null +++ b/src/components/ha-selector/ha-selector-location.ts @@ -0,0 +1,80 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import type { + LocationSelector, + LocationSelectorValue, +} from "../../data/selector"; +import "../../panels/lovelace/components/hui-theme-select-editor"; +import type { HomeAssistant } from "../../types"; +import type { MarkerLocation } from "../map/ha-locations-editor"; +import "../map/ha-locations-editor"; + +@customElement("ha-selector-location") +export class HaLocationSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: LocationSelector; + + @property() public value?: LocationSelectorValue; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } + + private _location = memoizeOne( + ( + selector: LocationSelector, + value?: LocationSelectorValue + ): MarkerLocation[] => { + const computedStyles = getComputedStyle(this); + const zoneRadiusColor = selector.location.radius + ? computedStyles.getPropertyValue("--zone-radius-color") || + computedStyles.getPropertyValue("--accent-color") + : undefined; + return [ + { + id: "location", + latitude: value?.latitude || this.hass.config.latitude, + longitude: value?.longitude || this.hass.config.longitude, + radius: selector.location.radius ? value?.radius || 1000 : undefined, + radius_color: zoneRadiusColor, + icon: selector.location.icon, + location_editable: true, + radius_editable: true, + }, + ]; + } + ); + + private _locationChanged(ev: CustomEvent) { + const [latitude, longitude] = ev.detail.location; + fireEvent(this, "value-changed", { + value: { ...this.value, latitude, longitude }, + }); + } + + private _radiusChanged(ev: CustomEvent) { + const radius = ev.detail.radius; + fireEvent(this, "value-changed", { value: { ...this.value, radius } }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-location": HaLocationSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 5f6a31aebc..61b790cd98 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -20,6 +20,7 @@ import "./ha-selector-time"; import "./ha-selector-icon"; import "./ha-selector-media"; import "./ha-selector-theme"; +import "./ha-selector-location"; @customElement("ha-selector") export class HaSelector extends LitElement { diff --git a/src/data/selector.ts b/src/data/selector.ts index d8cb9a4e63..c8cc87b36e 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -15,7 +15,8 @@ export type Selector = | SelectSelector | IconSelector | MediaSelector - | ThemeSelector; + | ThemeSelector + | LocationSelector; export interface EntitySelector { entity: { @@ -164,6 +165,16 @@ export interface MediaSelector { media: {}; } +export interface LocationSelector { + location: { radius?: boolean; icon?: string }; +} + +export interface LocationSelectorValue { + latitude: number; + longitude: number; + radius?: number; +} + export interface MediaSelectorValue { entity_id?: string; media_content_id?: string; diff --git a/src/data/zone.ts b/src/data/zone.ts index a4e091c30e..52ea733317 100644 --- a/src/data/zone.ts +++ b/src/data/zone.ts @@ -12,12 +12,12 @@ export interface Zone { } export interface ZoneMutableParams { + name: string; icon?: string; latitude: number; longitude: number; - name: string; - passive: boolean; - radius: number; + passive?: boolean; + radius?: number; } export const fetchZones = (hass: HomeAssistant) => diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index 81c3e78807..07b9fcb4fa 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -1,17 +1,12 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord"; -import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { createCloseHeading } from "../../../components/ha-dialog"; -import "../../../components/ha-formfield"; -import "../../../components/ha-icon-picker"; -import "../../../components/ha-switch"; -import "../../../components/map/ha-locations-editor"; -import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; +import "../../../components/ha-form/ha-form"; +import { HaFormSchema } from "../../../components/ha-form/types"; import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; @@ -20,19 +15,9 @@ import { ZoneDetailDialogParams } from "./show-dialog-zone-detail"; class DialogZoneDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _name!: string; + @state() private _error?: Record; - @state() private _icon!: string; - - @state() private _latitude!: number; - - @state() private _longitude!: number; - - @state() private _passive!: boolean; - - @state() private _radius!: number; - - @state() private _error?: string; + @state() private _data?: ZoneMutableParams; @state() private _params?: ZoneDetailDialogParams; @@ -42,13 +27,7 @@ class DialogZoneDetail extends LitElement { this._params = params; this._error = undefined; if (this._params.entry) { - this._name = this._params.entry.name || ""; - this._icon = this._params.entry.icon || ""; - this._latitude = this._params.entry.latitude || this.hass.config.latitude; - this._longitude = - this._params.entry.longitude || this.hass.config.longitude; - this._passive = this._params.entry.passive || false; - this._radius = this._params.entry.radius || 100; + this._data = this._params.entry; } else { const initConfig = getZoneEditorInitData(); let movedHomeLocation; @@ -59,30 +38,34 @@ class DialogZoneDetail extends LitElement { Math.random() * 500 * (Math.random() < 0.5 ? -1 : 1) ); } - this._latitude = initConfig?.latitude || movedHomeLocation[0]; - this._longitude = initConfig?.longitude || movedHomeLocation[1]; - this._name = initConfig?.name || ""; - this._icon = initConfig?.icon || "mdi:map-marker"; - - this._passive = false; - this._radius = 100; + this._data = { + latitude: initConfig?.latitude || movedHomeLocation[0], + longitude: initConfig?.longitude || movedHomeLocation[1], + name: initConfig?.name || "", + icon: initConfig?.icon || "mdi:map-marker", + passive: false, + radius: 100, + }; } } public closeDialog(): void { this._params = undefined; + this._data = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { - if (!this._params) { + if (!this._params || !this._data) { return html``; } - const nameInvalid = this._name.trim() === ""; - const iconInvalid = Boolean(this._icon && !this._icon.trim().includes(":")); - const latInvalid = String(this._latitude) === ""; - const lngInvalid = String(this._longitude) === ""; - const radiusInvalid = String(this._radius) === ""; + const nameInvalid = this._data.name.trim() === ""; + const iconInvalid = Boolean( + this._data.icon && !this._data.icon.trim().includes(":") + ); + const latInvalid = String(this._data.latitude) === ""; + const lngInvalid = String(this._data.longitude) === ""; + const radiusInvalid = String(this._data.radius) === ""; const valid = !nameInvalid && @@ -105,96 +88,15 @@ class DialogZoneDetail extends LitElement { )} >
- ${this._error ? html`
${this._error}
` : ""} -
- - - -
- - -
- -

- ${this.hass!.localize("ui.panel.config.zone.detail.passive_note")} -

- - - -
+
${this._params.entry ? html` @@ -221,74 +123,94 @@ class DialogZoneDetail extends LitElement { `; } - private _location = memoizeOne( - ( - lat: number, - lng: number, - radius: number, - passive: boolean, - icon: string - ): MarkerLocation[] => { - const computedStyles = getComputedStyle(this); - const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color"); - const passiveRadiusColor = computedStyles.getPropertyValue( - "--secondary-text-color" - ); - return [ + private _schema = memoizeOne((icon?: string): HaFormSchema[] => [ + { + name: "name", + required: true, + selector: { + text: {}, + }, + }, + { + name: "icon", + required: false, + selector: { + icon: {}, + }, + }, + { + name: "location", + required: true, + selector: { location: { radius: true, icon } }, + }, + { + name: "", + type: "grid", + schema: [ { - id: "location", - latitude: Number(lat), - longitude: Number(lng), - radius, - radius_color: passive ? passiveRadiusColor : zoneRadiusColor, - icon, - location_editable: true, - radius_editable: true, + name: "latitude", + required: true, + selector: { text: {} }, }, - ]; - } - ); + { + name: "longitude", + required: true, - private _locationChanged(ev: CustomEvent) { - [this._latitude, this._longitude] = ev.detail.location; - } + selector: { text: {} }, + }, + ], + }, + { name: "passive_note", type: "constant" }, + { name: "passive", selector: { boolean: {} } }, + { + name: "radius", + required: false, + selector: { number: { min: 0, max: 999999, mode: "box" } }, + }, + ]); - private _radiusChanged(ev: CustomEvent) { - this._radius = ev.detail.radius; - } - - private _passiveChanged(ev) { - this._passive = ev.target.checked; - } + private _formData = memoizeOne((data: ZoneMutableParams) => ({ + ...data, + location: { + latitude: data.latitude, + longitude: data.longitude, + radius: data.radius, + }, + })); private _valueChanged(ev: CustomEvent) { - const configValue = (ev.target as any).configValue; - this._error = undefined; - this[`_${configValue}`] = ev.detail.value; + const value = ev.detail.value; + if ( + value.location.latitude !== this._data!.latitude || + value.location.longitude !== this._data!.longitude || + value.location.radius !== this._data!.radius + ) { + value.latitude = value.location.latitude; + value.longitude = value.location.longitude; + value.radius = Math.round(value.location.radius); + } + delete value.location; + if (!value.icon) { + delete value.icon; + } + this._data = value; } + private _computeLabel = (entry: HaFormSchema): string => + this.hass.localize(`ui.panel.config.zone.detail.${entry.name}`); + private async _updateEntry() { this._submitting = true; try { - const values: ZoneMutableParams = { - name: this._name.trim(), - latitude: this._latitude, - longitude: this._longitude, - passive: this._passive, - radius: this._radius, - }; - if (this._icon) { - values.icon = this._icon.trim(); - } if (this._params!.entry) { - await this._params!.updateEntry!(values); + await this._params!.updateEntry!(this._data!); } else { - await this._params!.createEntry(values); + await this._params!.createEntry(this._data!); } - this._params = undefined; + this.closeDialog(); } catch (err: any) { - this._error = err ? err.message : "Unknown error"; + this._error = { base: err ? err.message : "Unknown error" }; } finally { this._submitting = false; } @@ -309,24 +231,18 @@ class DialogZoneDetail extends LitElement { return [ haStyleDialog, css` - .location { - display: flex; + ha-dialog { + --mdc-dialog-min-width: 600px; } - .location > * { - flex-grow: 1; - min-width: 0; + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --mdc-dialog-min-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + } } - .location > *:first-child { - margin-right: 4px; - } - .location > *:last-child { - margin-left: 4px; - } - ha-locations-editor { - margin-top: 16px; - } - a { - color: var(--primary-color); + ha-form.passive { + --zone-radius-color: var(--secondary-text-color); } `, ];