-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Map: add marker badges #51435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Map: add marker badges #51435
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
a994cb2
add MapMarkerBadgeConfig
ildar170975 d13ec4f
add badge
ildar170975 28e7086
Create ha-map-marker-badge.ts
ildar170975 2a84ef2
add map marker badges
ildar170975 9a7394b
add map marker badges
ildar170975 6130de7
add map marker badge
ildar170975 a96b635
prettier
ildar170975 223894b
prettier
ildar170975 ad724f6
prettier
ildar170975 8f93109
Merge branch 'dev' into map-marker-badge
ildar170975 0ae7ae8
resolving conflicts
ildar170975 66349d6
label_mode -> display_mode
ildar170975 cef74b3
prettier
ildar170975 1a6c7c3
revert: display_mode -> label_mode
ildar170975 9be11b7
update mapBadgeConfigStruct
ildar170975 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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,281 @@ | ||
| import { mdiAlert } from "@mdi/js"; | ||
| import { css, html, LitElement, nothing } from "lit"; | ||
| import { customElement, property } from "lit/decorators"; | ||
| import { classMap } from "lit/directives/class-map"; | ||
| import { styleMap } from "lit/directives/style-map"; | ||
| import { until } from "lit/directives/until"; | ||
| import { boolean, number, object, optional, string, union } from "superstruct"; | ||
| import { fireEvent } from "../../common/dom/fire_event"; | ||
| import { orderProperties } from "../../common/util/order-properties"; | ||
| import { entityIcon } from "../../data/icons"; | ||
| import type { HomeAssistant } from "../../types"; | ||
| import "../entity/state-badge"; | ||
| import "../ha-svg-icon"; | ||
|
|
||
| export const MAP_CARD_BADGE_LABEL_MODES = [ | ||
| "label", | ||
| "state", | ||
| "attribute", | ||
| "icon", | ||
| "image", | ||
| ] as const; | ||
| export type MapCardBadgeLabelMode = (typeof MAP_CARD_BADGE_LABEL_MODES)[number]; | ||
|
|
||
| export interface MapMarkerBadgeConfig { | ||
| // entity_id to be processed | ||
| entity?: string; | ||
| label_mode?: MapCardBadgeLabelMode; | ||
| // only processed if `label_mode: label`; used to display a state/attribute value or any text | ||
| label?: string; | ||
| // chooses an attribute; only processed if `label_mode: attribute` | ||
| attribute?: string; | ||
| // sets a unit for an attribute value; only processed if `label_mode: attribute` | ||
| unit?: string; | ||
| // overrides an `entity_picture` if an `entity` is defined; or set an image if no `entity` defined | ||
| image?: string; | ||
| // overrides an entity icon if an `entity` is defined; or set an icon if no `entity` defined | ||
| icon?: string; | ||
| // affects label & icon | ||
| color?: string; | ||
| // as it says | ||
| background_color?: string; | ||
| // similar to other cards; only processed for domains which support colors | ||
| state_color?: boolean; | ||
| // omits a unit for compactness; applied both to a state & attribute | ||
| hide_unit?: boolean; | ||
| } | ||
|
|
||
| export const mapBadgeConfigStruct = object({ | ||
| entity: optional(string()), | ||
| label_mode: optional(string()), | ||
| label: optional(union([string(), number()])), // allow values like "label: 123" | ||
| attribute: optional(string()), | ||
| unit: optional(string()), | ||
| icon: optional(string()), | ||
| image: optional(union([string(), object()])), | ||
| color: optional(string()), | ||
| background_color: optional(string()), | ||
| state_color: optional(boolean()), | ||
| hide_unit: optional(boolean()), | ||
| }); | ||
|
|
||
| // normalize a generated yaml code by placing lines in a consistent order | ||
| export const mapMarkerBadgeOrderProperties = ( | ||
| config: MapMarkerBadgeConfig | ||
| ): MapMarkerBadgeConfig => { | ||
| const fieldOrderBadge = Object.keys(mapBadgeConfigStruct.schema); | ||
| const orderedConfig = { ...orderProperties(config, fieldOrderBadge) }; | ||
| return orderedConfig; | ||
| }; | ||
|
|
||
| @customElement("ha-map-marker-badge") | ||
| export class HaMapMarkerBadge extends LitElement { | ||
| @property({ attribute: false }) public hass!: HomeAssistant; | ||
|
|
||
| @property({ attribute: false }) public badge!: MapMarkerBadgeConfig; | ||
|
|
||
| @property({ attribute: "border_color" }) public borderColor?: string; | ||
|
|
||
| protected render() { | ||
| const label_mode = this.badge.label_mode; | ||
| const stateObj = this.badge.entity | ||
| ? this.hass.states[this.badge.entity] | ||
| : undefined; | ||
|
|
||
| let icon = this.badge.icon; | ||
| if ( | ||
| !icon && | ||
| stateObj && | ||
| stateObj.attributes.entity_picture && | ||
| label_mode === "icon" | ||
| ) { | ||
| icon = stateObj?.attributes.icon; | ||
| if (!icon) { | ||
| icon = until(entityIcon(this.hass, stateObj)); | ||
| } | ||
| } | ||
|
|
||
| let label; | ||
| if (label_mode === "label") { | ||
| label = this.badge.label; | ||
| } else if (label_mode === "state" && stateObj) { | ||
| if (this.badge.hide_unit) { | ||
| const stateParts = this.hass.formatEntityStateToParts(stateObj); | ||
| label = stateParts | ||
| .filter((part) => part.type === "value") | ||
| .map((part) => part.value) | ||
| .join(""); | ||
| } else { | ||
| label = this.hass.formatEntityState(stateObj); | ||
| } | ||
| } else if (label_mode === "attribute" && this.badge.attribute && stateObj) { | ||
| if (this.badge.hide_unit) { | ||
| const attrParts = this.hass.formatEntityAttributeValueToParts( | ||
| stateObj, | ||
| this.badge.attribute | ||
| ); | ||
| label = attrParts | ||
| .filter((part) => part.type === "value") | ||
| .map((part) => part.value) | ||
| .join(""); | ||
| } else { | ||
| label = this.hass.formatEntityAttributeValue( | ||
| stateObj, | ||
| this.badge.attribute | ||
| ); | ||
| if (this.badge.unit) { | ||
| const composed = `${label} ${this.badge.unit}`; | ||
| label = composed; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const clsImageOnly = | ||
| label_mode === "image" && this.badge.image && !stateObj; | ||
| const clsLabel = | ||
| ((label_mode === "state" || | ||
| (label_mode === "attribute" && this.badge.attribute)) && | ||
| stateObj) || | ||
| (label_mode === "label" && this.badge.label); | ||
|
|
||
| const error = | ||
| (!label_mode && !stateObj) || | ||
| (label_mode === "label" && !this.badge.label) || | ||
| (label_mode === "state" && !stateObj) || | ||
| (label_mode === "attribute" && !stateObj) || | ||
| (label_mode === "attribute" && !this.badge.attribute); | ||
|
|
||
| return html` | ||
| <div | ||
| class=${classMap({ | ||
| badge: true, | ||
| "image-only": clsImageOnly, | ||
| label: clsLabel, | ||
| colored: | ||
| this.badge.color && | ||
| !error && | ||
| (((!label_mode || label_mode === "icon") && | ||
| !this.badge.state_color) || | ||
| clsLabel), | ||
| })} | ||
| style=${styleMap({ | ||
| "border-color": this.borderColor, | ||
| "--color": !this.badge.state_color ? this.badge.color : undefined, | ||
| "--background-color": this.badge.background_color, | ||
| "background-image": clsImageOnly | ||
| ? `url(${this.hass.hassUrl(this.badge.image)})` | ||
| : undefined, | ||
| "--font-size": this.badge.hide_unit | ||
| ? `var(--ha-font-size-m)` | ||
| : `var(--ha-font-size-xs)`, | ||
| })} | ||
| @click=${this._badgeTap} | ||
| > | ||
| ${!label_mode && stateObj | ||
| ? html`<state-badge | ||
| .hass=${this.hass} | ||
| .stateObj=${stateObj} | ||
| .overrideIcon=${icon} | ||
| .overrideImage=${this.badge.image} | ||
| .stateColor=${this.badge.state_color} | ||
| ></state-badge>` | ||
| : nothing} | ||
| ${label_mode === "icon" | ||
| ? html`<state-badge | ||
| .hass=${this.hass} | ||
| .stateObj=${stateObj} | ||
| .overrideIcon=${icon} | ||
| .stateColor=${this.badge.state_color} | ||
| ></state-badge>` | ||
| : nothing} | ||
| ${label_mode === "image" && stateObj | ||
| ? html`<state-badge | ||
| .hass=${this.hass} | ||
| .stateObj=${stateObj} | ||
| .overrideImage=${this.badge.image} | ||
| ></state-badge>` | ||
| : nothing} | ||
| ${clsLabel ? label : nothing} | ||
| ${error | ||
| ? html`<div class="error"> | ||
| <ha-svg-icon .path=${mdiAlert}></ha-svg-icon> | ||
| </div>` | ||
| : nothing} | ||
| </div> | ||
| `; | ||
| } | ||
|
|
||
| private _badgeTap(ev: Event) { | ||
| ev.stopPropagation(); | ||
| if (this.badge.entity) { | ||
| fireEvent(this, "hass-more-info", { entityId: this.badge.entity }); | ||
| } | ||
| } | ||
|
|
||
| static styles = css` | ||
| :host { | ||
| position: absolute; | ||
| --badge-ratio: 2.5; /* ratio of marker size to badge size */ | ||
| --icon-ratio: 1.5; /* ratio of badge size to icon size */ | ||
|
|
||
| --mdc-icon-size: calc( | ||
| var(--ha-marker-size, 48px) / var(--badge-ratio) / var(--icon-ratio) | ||
| ); | ||
|
|
||
| /* badge size used to define width & height */ | ||
| --badge-size: calc(var(--ha-marker-size, 48px) / var(--badge-ratio)); | ||
|
|
||
| top: calc(var(--badge-size) * 0.25 * -1); | ||
| left: calc(var(--ha-marker-size, 48px) - var(--badge-size) * 0.75); | ||
| inset-inline-start: calc( | ||
| var(--ha-marker-size, 48px) - var(--badge-size) * 0.75 | ||
| ); | ||
| inset-inline-end: initial; | ||
| } | ||
| .badge { | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| line-height: 0; | ||
| width: var(--badge-size); | ||
| height: var(--badge-size); | ||
| box-sizing: border-box; | ||
| border: 1px solid var(--ha-marker-color, var(--primary-color)); | ||
| border-radius: var(--ha-marker-badge-border-radius, 50%); | ||
| background-color: var(--background-color, var(--card-background-color)); | ||
| transition: background-color 280ms ease-in-out; | ||
| } | ||
| .image-only { | ||
| background-size: cover; | ||
| background-repeat: no-repeat; | ||
| background-position: center; | ||
| } | ||
| .label { | ||
| /* 0.7 - coefficient to get smaller fonts than ha-font-size-xs */ | ||
| font-size: var( | ||
| --ha-marker-badge-font-size, | ||
| calc(var(--font-size) * 0.7 * var(--ha-marker-size, 48px) / 48px) | ||
| ); | ||
| font-weight: var(--ha-font-weight-light); | ||
| text-align: center; | ||
| line-height: var(--ha-line-height-condensed); | ||
| } | ||
| state-badge { | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
| .colored.label, | ||
| .colored state-badge { | ||
| color: var(--color); | ||
| } | ||
| .error { | ||
| color: #fce588; /* same color used in state-badge for "missing" class */ | ||
| } | ||
| `; | ||
| } | ||
|
|
||
| declare global { | ||
| interface HTMLElementTagNameMap { | ||
| "ha-map-marker-badge": HaMapMarkerBadge; | ||
| } | ||
| } | ||
This file contains hidden or 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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a CSS injection risk.
ha-entity-marker.tsalready does this so may not be a real problem though. Have to think about itThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
State-badge uses same
this.hass.hassUrl()thing... Imho the risk should be minimized in thehassUrlfunction.