mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			20250430.0
			...
			card_edito
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 76380c189b | ||
|   | c8b7f373c3 | ||
|   | 4f2652abd2 | ||
|   | 6d8b7f6995 | ||
|   | bbf8a8e3e7 | ||
|   | 14308c9057 | ||
|   | b87f44ff74 | ||
|   | 36540aa8fb | 
| @@ -1,4 +1,5 @@ | ||||
| import { Button } from "@material/mwc-button"; | ||||
| import { Corner } from "@material/web/menu/menu"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; | ||||
| @@ -14,8 +15,20 @@ export class HaButtonMenuNew extends LitElement { | ||||
|  | ||||
|   @property() public positioning?: "fixed" | "absolute" | "popover"; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow = | ||||
|     false; | ||||
|   @property({ type: Boolean, attribute: "no-horizontal-flip" }) | ||||
|   public noHorizontalFlip = false; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "no-vertical-flip" }) | ||||
|   public noVerticalFlip = false; | ||||
|  | ||||
|   @property({ attribute: "anchor-corner" }) | ||||
|   public anchorCorner: Corner = Corner.END_START; | ||||
|  | ||||
|   @property({ attribute: "menu-corner" }) | ||||
|   public menuCorner: Corner = Corner.START_START; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "has-overflow" }) | ||||
|   public hasOverflow = false; | ||||
|  | ||||
|   @query("ha-menu", true) private _menu!: HaMenu; | ||||
|  | ||||
| @@ -39,6 +52,10 @@ export class HaButtonMenuNew extends LitElement { | ||||
|       <ha-menu | ||||
|         .positioning=${this.positioning} | ||||
|         .hasOverflow=${this.hasOverflow} | ||||
|         .anchorCorner=${this.anchorCorner} | ||||
|         .menuCorner=${this.menuCorner} | ||||
|         .noVerticalFlip=${this.noVerticalFlip} | ||||
|         .noHorizontalFlip=${this.noHorizontalFlip} | ||||
|       > | ||||
|         <slot></slot> | ||||
|       </ha-menu> | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/components/ha-outlined-segmented-button-set.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/ha-outlined-segmented-button-set.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { MdOutlinedSegmentedButtonSet } from "@material/web/labs/segmentedbuttonset/outlined-segmented-button-set"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-outlined-segmented-button-set") | ||||
| export class HaOutlinedSegmentedButtonSet extends MdOutlinedSegmentedButtonSet { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
|       :host { | ||||
|         --ha-icon-display: block; | ||||
|         --md-outlined-segmented-button-container-height: 32px; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-outlined-segmented-button-set": HaOutlinedSegmentedButtonSet; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/components/ha-outlined-segmented-button.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/components/ha-outlined-segmented-button.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import { MdOutlinedSegmentedButton } from "@material/web/labs/segmentedbutton/outlined-segmented-button"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-outlined-segmented-button") | ||||
| export class HaOutlinedSegmentedButton extends MdOutlinedSegmentedButton { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
|       :host { | ||||
|         --ha-icon-display: block; | ||||
|         --md-outlined-segmented-button-selected-container-color: var( | ||||
|           --light-primary-color | ||||
|         ); | ||||
|         --md-outlined-segmented-button-container-height: 32px; | ||||
|         --md-outlined-segmented-button-disabled-label-text-color: var( | ||||
|           --disabled-text-color | ||||
|         ); | ||||
|         --md-outlined-segmented-button-disabled-icon-color: var( | ||||
|           --disabled-text-color | ||||
|         ); | ||||
|         --md-outlined-segmented-button-disabled-outline-color: var( | ||||
|           --disabled-text-color | ||||
|         ); | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-outlined-segmented-button": HaOutlinedSegmentedButton; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/panels/lovelace/common/compute-card-name.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/panels/lovelace/common/compute-card-name.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { LocalizeFunc } from "../../../common/translations/localize"; | ||||
| import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import { | ||||
|   getCustomCardEntry, | ||||
|   isCustomType, | ||||
|   stripCustomPrefix, | ||||
| } from "../../../data/lovelace_custom_cards"; | ||||
|  | ||||
| export const computeCardName = ( | ||||
|   config: LovelaceCardConfig, | ||||
|   localize: LocalizeFunc | ||||
| ): string | undefined => { | ||||
|   if (isCustomType(config.type)) { | ||||
|     // prettier-ignore | ||||
|     let cardName = getCustomCardEntry( | ||||
|           stripCustomPrefix(config.type) | ||||
|         )?.name; | ||||
|     // Trim names that end in " Card" so as not to redundantly duplicate it | ||||
|     if (cardName?.toLowerCase().endsWith(" card")) { | ||||
|       cardName = cardName.substring(0, cardName.length - 5); | ||||
|     } | ||||
|     return cardName; | ||||
|   } | ||||
|   return localize(`ui.panel.lovelace.editor.card.${config.type}.name`); | ||||
| }; | ||||
							
								
								
									
										155
									
								
								src/panels/lovelace/editor/card-editor/hui-card-editor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/panels/lovelace/editor/card-editor/hui-card-editor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| import "@material/mwc-tab-bar/mwc-tab-bar"; | ||||
| import "@material/mwc-tab/mwc-tab"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; | ||||
| import { LovelaceConfig } from "../../../../data/lovelace/config/types"; | ||||
| import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import "./hui-card-element-editor"; | ||||
| import type { HuiCardElementEditor } from "./hui-card-element-editor"; | ||||
| import "./hui-card-layout-editor"; | ||||
| import "./hui-card-visibility-editor"; | ||||
|  | ||||
| const TABS = ["config", "visibility", "layout"] as const; | ||||
|  | ||||
| @customElement("hui-card-editor") | ||||
| class HuiCardEditor extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public lovelace!: LovelaceConfig; | ||||
|  | ||||
|   @property({ attribute: false }) public config!: LovelaceCardConfig; | ||||
|  | ||||
|   @property({ attribute: false }) public containerConfig!: | ||||
|     | LovelaceViewConfig | ||||
|     | LovelaceSectionConfig; | ||||
|  | ||||
|   @query("hui-card-element-editor") | ||||
|   public elementEditor?: HuiCardElementEditor; | ||||
|  | ||||
|   @state() private _selectedTab: (typeof TABS)[number] = TABS[0]; | ||||
|  | ||||
|   private _tabs = memoizeOne( | ||||
|     (containerType: string | undefined, cardType: string) => | ||||
|       TABS.filter((tab) => { | ||||
|         if (tab === "visibility") return cardType !== "conditional"; | ||||
|         if (tab === "layout") return containerType === "grid"; | ||||
|         return true; | ||||
|       }) | ||||
|   ); | ||||
|  | ||||
|   private _elementConfig = memoizeOne((config: LovelaceCardConfig) => { | ||||
|     const { visibility, layout_options, ...elementConfig } = config; | ||||
|     return elementConfig; | ||||
|   }); | ||||
|  | ||||
|   private renderContent() { | ||||
|     if (this._selectedTab === "config") { | ||||
|       return html` | ||||
|         <hui-card-element-editor | ||||
|           .hass=${this.hass} | ||||
|           .lovelace=${this.lovelace} | ||||
|           .value=${this._elementConfig(this.config)} | ||||
|           show-toggle-mode-button | ||||
|           @config-changed=${this._elementConfigChanged} | ||||
|         ></hui-card-element-editor> | ||||
|       `; | ||||
|     } | ||||
|     if (this._selectedTab === "visibility") { | ||||
|       return html` | ||||
|         <hui-card-visibility-editor | ||||
|           .hass=${this.hass} | ||||
|           .config=${this.config} | ||||
|           @value-changed=${this._configChanged} | ||||
|         ></hui-card-visibility-editor> | ||||
|       `; | ||||
|     } | ||||
|     if (this._selectedTab === "layout") { | ||||
|       return html` | ||||
|         <hui-card-layout-editor | ||||
|           .hass=${this.hass} | ||||
|           .config=${this.config} | ||||
|           .sectionConfig=${this.containerConfig as LovelaceSectionConfig} | ||||
|           @value-changed=${this._configChanged} | ||||
|         > | ||||
|         </hui-card-layout-editor> | ||||
|       `; | ||||
|     } | ||||
|     return nothing; | ||||
|   } | ||||
|  | ||||
|   private _configChanged(ev: CustomEvent): void { | ||||
|     ev.stopPropagation(); | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _elementConfigChanged(ev: CustomEvent): void { | ||||
|     ev.stopPropagation(); | ||||
|     const config = ev.detail.config; | ||||
|     const newConfig = { | ||||
|       ...config, | ||||
|       visibility: this.config.visibility, | ||||
|       layout_options: this.config.layout_options, | ||||
|     }; | ||||
|     fireEvent(this, "config-changed", { config: newConfig }); | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     const cardType = this.config.type; | ||||
|     const containerType = this.containerConfig.type; | ||||
|     const tabs = this._tabs(containerType, cardType); | ||||
|  | ||||
|     if (tabs.length <= 1) { | ||||
|       return this.renderContent(); | ||||
|     } | ||||
|     return html` | ||||
|       <mwc-tab-bar | ||||
|         .activeIndex=${tabs.indexOf(this._selectedTab)} | ||||
|         @MDCTabBar:activated=${this._handleTabChanged} | ||||
|       > | ||||
|         ${tabs.map( | ||||
|           (tab) => html` | ||||
|             <mwc-tab | ||||
|               .label=${this.hass.localize( | ||||
|                 `ui.panel.lovelace.editor.edit_card.tab_${tab}` | ||||
|               )} | ||||
|             > | ||||
|             </mwc-tab> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-tab-bar> | ||||
|       ${this.renderContent()} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _handleTabChanged(ev: CustomEvent): void { | ||||
|     const cardType = this.config.type; | ||||
|     const containerType = this.containerConfig.type; | ||||
|     const tabs = this._tabs(containerType, cardType); | ||||
|     const newTab = tabs[ev.detail.index]; | ||||
|     if (newTab === this._selectedTab) { | ||||
|       return; | ||||
|     } | ||||
|     this._selectedTab = newTab; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       mwc-tab-bar { | ||||
|         text-transform: uppercase; | ||||
|         margin-bottom: 16px; | ||||
|         border-bottom: 1px solid var(--divider-color); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hui-card-editor": HuiCardEditor; | ||||
|   } | ||||
| } | ||||
| @@ -1,26 +1,11 @@ | ||||
| import "@material/mwc-tab-bar/mwc-tab-bar"; | ||||
| import "@material/mwc-tab/mwc-tab"; | ||||
| import { CSSResultGroup, TemplateResult, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { getCardElementClass } from "../../create-element/create-card-element"; | ||||
| import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types"; | ||||
| import { HuiElementEditor } from "../hui-element-editor"; | ||||
| import "./hui-card-layout-editor"; | ||||
| import "./hui-card-visibility-editor"; | ||||
| import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; | ||||
|  | ||||
| const tabs = ["config", "visibility", "layout"] as const; | ||||
|  | ||||
| @customElement("hui-card-element-editor") | ||||
| export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> { | ||||
|   @property({ type: Boolean, attribute: "show-visibility-tab" }) | ||||
|   public showVisibilityTab = false; | ||||
|  | ||||
|   @property({ attribute: false }) public sectionConfig?: LovelaceSectionConfig; | ||||
|  | ||||
|   @state() private _currTab: (typeof tabs)[number] = tabs[0]; | ||||
|  | ||||
|   protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> { | ||||
|     const elClass = await getCardElementClass(this.configElementType!); | ||||
|  | ||||
| @@ -42,93 +27,6 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> { | ||||
|  | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   private _configChanged(ev: CustomEvent): void { | ||||
|     ev.stopPropagation(); | ||||
|     this.value = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   get _showLayoutTab(): boolean { | ||||
|     return ( | ||||
|       !!this.sectionConfig && | ||||
|       (this.sectionConfig.type === undefined || | ||||
|         this.sectionConfig.type === "grid") | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   protected renderConfigElement(): TemplateResult { | ||||
|     const displayedTabs: string[] = ["config"]; | ||||
|     if (this.showVisibilityTab) displayedTabs.push("visibility"); | ||||
|     if (this._showLayoutTab) displayedTabs.push("layout"); | ||||
|  | ||||
|     if (displayedTabs.length === 1) return super.renderConfigElement(); | ||||
|  | ||||
|     let content: TemplateResult<1> | typeof nothing = nothing; | ||||
|  | ||||
|     switch (this._currTab) { | ||||
|       case "config": | ||||
|         content = html`${super.renderConfigElement()}`; | ||||
|         break; | ||||
|       case "visibility": | ||||
|         content = html` | ||||
|           <hui-card-visibility-editor | ||||
|             .hass=${this.hass} | ||||
|             .config=${this.value} | ||||
|             @value-changed=${this._configChanged} | ||||
|           ></hui-card-visibility-editor> | ||||
|         `; | ||||
|         break; | ||||
|       case "layout": | ||||
|         content = html` | ||||
|           <hui-card-layout-editor | ||||
|             .hass=${this.hass} | ||||
|             .config=${this.value} | ||||
|             .sectionConfig=${this.sectionConfig!} | ||||
|             @value-changed=${this._configChanged} | ||||
|           > | ||||
|           </hui-card-layout-editor> | ||||
|         `; | ||||
|     } | ||||
|     return html` | ||||
|       <mwc-tab-bar | ||||
|         .activeIndex=${tabs.indexOf(this._currTab)} | ||||
|         @MDCTabBar:activated=${this._handleTabChanged} | ||||
|       > | ||||
|         ${displayedTabs.map( | ||||
|           (tab) => html` | ||||
|             <mwc-tab | ||||
|               .label=${this.hass.localize( | ||||
|                 `ui.panel.lovelace.editor.edit_card.tab_${tab}` | ||||
|               )} | ||||
|             > | ||||
|             </mwc-tab> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-tab-bar> | ||||
|       ${content} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _handleTabChanged(ev: CustomEvent): void { | ||||
|     const newTab = tabs[ev.detail.index]; | ||||
|     if (newTab === this._currTab) { | ||||
|       return; | ||||
|     } | ||||
|     this._currTab = newTab; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       HuiElementEditor.styles, | ||||
|       css` | ||||
|         mwc-tab-bar { | ||||
|           text-transform: uppercase; | ||||
|           margin-bottom: 16px; | ||||
|           border-bottom: 1px solid var(--divider-color); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,5 +1,14 @@ | ||||
| import { mdiClose, mdiHelpCircle } from "@mdi/js"; | ||||
| import "@material/mwc-list"; | ||||
| import "@material/web/divider/divider"; | ||||
| import { | ||||
|   mdiCheck, | ||||
|   mdiClose, | ||||
|   mdiDotsVertical, | ||||
|   mdiHelpCircle, | ||||
|   mdiOpenInNew, | ||||
| } from "@mdi/js"; | ||||
| import deepFreeze from "deep-freeze"; | ||||
| import { dump, load } from "js-yaml"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
| @@ -12,32 +21,30 @@ import { customElement, property, query, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeRTLDirection } from "../../../../common/util/compute_rtl"; | ||||
| import "../../../../components/ha-button"; | ||||
| import "../../../../components/ha-button-menu-new"; | ||||
| import "../../../../components/ha-circular-progress"; | ||||
| import "../../../../components/ha-code-editor"; | ||||
| import "../../../../components/ha-dialog"; | ||||
| import "../../../../components/ha-dialog-header"; | ||||
| import "../../../../components/ha-icon-button"; | ||||
| import "../../../../components/ha-menu-item"; | ||||
| import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; | ||||
| import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; | ||||
| import { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; | ||||
| import { | ||||
|   getCustomCardEntry, | ||||
|   isCustomType, | ||||
|   stripCustomPrefix, | ||||
| } from "../../../../data/lovelace_custom_cards"; | ||||
| import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; | ||||
| import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; | ||||
| import { haStyleDialog } from "../../../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; | ||||
| import "../../cards/hui-card"; | ||||
| import { computeCardName } from "../../common/compute-card-name"; | ||||
| import "../../sections/hui-section"; | ||||
| import { addCard, replaceCard } from "../config-util"; | ||||
| import { getCardDocumentationURL } from "../get-dashboard-documentation-url"; | ||||
| import type { ConfigChangedEvent } from "../hui-element-editor"; | ||||
| import { findLovelaceContainer } from "../lovelace-path"; | ||||
| import type { GUIModeChangedEvent } from "../types"; | ||||
| import "./hui-card-element-editor"; | ||||
| import "./hui-card-editor"; | ||||
| import type { HuiCardElementEditor } from "./hui-card-element-editor"; | ||||
| import type { EditCardDialogParams } from "./show-edit-card-dialog"; | ||||
|  | ||||
| @@ -73,12 +80,10 @@ export class HuiDialogEditCard | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   @state() private _guiModeAvailable? = true; | ||||
|  | ||||
|   @query("hui-card-element-editor") | ||||
|   @query("hui-card-editor") | ||||
|   private _cardEditorEl?: HuiCardElementEditor; | ||||
|  | ||||
|   @state() private _GUImode = true; | ||||
|   @state() private _yamlMode = false; | ||||
|  | ||||
|   @state() private _documentationURL?: string; | ||||
|  | ||||
| @@ -88,8 +93,7 @@ export class HuiDialogEditCard | ||||
|  | ||||
|   public async showDialog(params: EditCardDialogParams): Promise<void> { | ||||
|     this._params = params; | ||||
|     this._GUImode = true; | ||||
|     this._guiModeAvailable = true; | ||||
|     this._yamlMode = false; | ||||
|  | ||||
|     const containerConfig = findLovelaceContainer( | ||||
|       params.lovelaceConfig, | ||||
| @@ -168,21 +172,7 @@ export class HuiDialogEditCard | ||||
|  | ||||
|     let heading: string; | ||||
|     if (this._cardConfig && this._cardConfig.type) { | ||||
|       let cardName: string | undefined; | ||||
|       if (isCustomType(this._cardConfig.type)) { | ||||
|         // prettier-ignore | ||||
|         cardName = getCustomCardEntry( | ||||
|           stripCustomPrefix(this._cardConfig.type) | ||||
|         )?.name; | ||||
|         // Trim names that end in " Card" so as not to redundantly duplicate it | ||||
|         if (cardName?.toLowerCase().endsWith(" card")) { | ||||
|           cardName = cardName.substring(0, cardName.length - 5); | ||||
|         } | ||||
|       } else { | ||||
|         cardName = this.hass!.localize( | ||||
|           `ui.panel.lovelace.editor.card.${this._cardConfig.type}.name` | ||||
|         ); | ||||
|       } | ||||
|       const cardName = computeCardName(this._cardConfig, this.hass!.localize); | ||||
|       heading = this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.edit_card.typed_header", | ||||
|         { type: cardName } | ||||
| @@ -218,36 +208,99 @@ export class HuiDialogEditCard | ||||
|             .path=${mdiClose} | ||||
|           ></ha-icon-button> | ||||
|           <span slot="title" @click=${this._enlarge}>${heading}</span> | ||||
|           ${this._documentationURL !== undefined | ||||
|             ? html` | ||||
|                 <a | ||||
|                   slot="actionItems" | ||||
|                   href=${this._documentationURL} | ||||
|                   title=${this.hass!.localize("ui.panel.lovelace.menu.help")} | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                   dir=${computeRTLDirection(this.hass)} | ||||
|                 > | ||||
|                   <ha-icon-button .path=${mdiHelpCircle}></ha-icon-button> | ||||
|                 </a> | ||||
|               ` | ||||
|             : nothing} | ||||
|           <ha-button-menu-new | ||||
|             slot="actionItems" | ||||
|             anchor-corner="end-end" | ||||
|             menu-corner="start-end" | ||||
|           > | ||||
|             <ha-icon-button | ||||
|               slot="trigger" | ||||
|               .label=${this.hass.localize("ui.common.menu")} | ||||
|               .path=${mdiDotsVertical} | ||||
|             > | ||||
|             </ha-icon-button> | ||||
|             <ha-menu-item @click=${this._enableGuiMode}> | ||||
|               ${!this._yamlMode | ||||
|                 ? html` | ||||
|                     <ha-svg-icon | ||||
|                       class="selected_menu_item" | ||||
|                       slot="start" | ||||
|                       .path=${mdiCheck} | ||||
|                     ></ha-svg-icon> | ||||
|                   ` | ||||
|                 : html`<span class="blank-icon" slot="start"></span>`} | ||||
|               <div slot="headline"> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.lovelace.editor.edit_card.edit_ui" | ||||
|                 )} | ||||
|               </div> | ||||
|             </ha-menu-item> | ||||
|             <ha-menu-item @click=${this._enableYamlMode}> | ||||
|               ${this._yamlMode | ||||
|                 ? html` | ||||
|                     <ha-svg-icon | ||||
|                       class="selected_menu_item" | ||||
|                       slot="start" | ||||
|                       .path=${mdiCheck} | ||||
|                     ></ha-svg-icon> | ||||
|                   ` | ||||
|                 : html`<span class="blank-icon" slot="start"></span>`} | ||||
|               <div slot="headline"> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.lovelace.editor.edit_card.edit_yaml" | ||||
|                 )} | ||||
|               </div> | ||||
|             </ha-menu-item> | ||||
|             ${this._documentationURL !== undefined | ||||
|               ? html` | ||||
|                   <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|                   <ha-menu-item | ||||
|                     type="link" | ||||
|                     href=${this._documentationURL} | ||||
|                     target="_blank" | ||||
|                     rel="noreferrer" | ||||
|                   > | ||||
|                     <ha-svg-icon | ||||
|                       slot="start" | ||||
|                       .path=${mdiHelpCircle} | ||||
|                     ></ha-svg-icon> | ||||
|                     <div slot="headline"> | ||||
|                       ${this.hass!.localize("ui.panel.lovelace.menu.help")} | ||||
|                     </div> | ||||
|                     <ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon> | ||||
|                   </ha-menu-item> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           </ha-button-menu-new> | ||||
|         </ha-dialog-header> | ||||
|         <div class="content"> | ||||
|           <div class="element-editor"> | ||||
|             <hui-card-element-editor | ||||
|               .showVisibilityTab=${this._cardConfig?.type !== "conditional"} | ||||
|               .sectionConfig=${this._isInSection | ||||
|                 ? this._containerConfig | ||||
|                 : undefined} | ||||
|               .hass=${this.hass} | ||||
|               .lovelace=${this._params.lovelaceConfig} | ||||
|               .value=${this._cardConfig} | ||||
|               @config-changed=${this._handleConfigChanged} | ||||
|               @GUImode-changed=${this._handleGUIModeChanged} | ||||
|               @editor-save=${this._save} | ||||
|               dialogInitialFocus | ||||
|             ></hui-card-element-editor> | ||||
|             ${this._yamlMode | ||||
|               ? html` | ||||
|                   <ha-code-editor | ||||
|                     mode="yaml" | ||||
|                     autofocus | ||||
|                     autocomplete-entities | ||||
|                     autocomplete-icons | ||||
|                     .hass=${this.hass} | ||||
|                     .value=${dump(this._cardConfig)} | ||||
|                     @value-changed=${this._handleYAMLChanged} | ||||
|                     @keydown=${this._ignoreKeydown} | ||||
|                     dir="ltr" | ||||
|                   ></ha-code-editor> | ||||
|                 ` | ||||
|               : html` | ||||
|                   <hui-card-editor | ||||
|                     .containerConfig=${this._containerConfig} | ||||
|                     .hass=${this.hass} | ||||
|                     .lovelace=${this._params.lovelaceConfig} | ||||
|                     .config=${this._cardConfig} | ||||
|                     @config-changed=${this._handleConfigChanged} | ||||
|                     @editor-save=${this._save} | ||||
|                     dialogInitialFocus | ||||
|                   > | ||||
|                   </hui-card-editor> | ||||
|                 `} | ||||
|           </div> | ||||
|           <div class="element-preview"> | ||||
|             ${this._isInSection | ||||
| @@ -277,49 +330,44 @@ export class HuiDialogEditCard | ||||
|               : ``} | ||||
|           </div> | ||||
|         </div> | ||||
|         ${this._cardConfig !== undefined | ||||
|         <ha-button | ||||
|           @click=${this._cancel} | ||||
|           slot="secondaryAction" | ||||
|           dialogInitialFocus | ||||
|         > | ||||
|           ${this.hass!.localize("ui.common.cancel")} | ||||
|         </ha-button> | ||||
|         ${this._cardConfig !== undefined && this._dirty | ||||
|           ? html` | ||||
|               <mwc-button | ||||
|                 slot="secondaryAction" | ||||
|                 @click=${this._toggleMode} | ||||
|                 .disabled=${!this._guiModeAvailable} | ||||
|                 class="gui-mode-button" | ||||
|               <ha-button | ||||
|                 slot="primaryAction" | ||||
|                 ?disabled=${!this._canSave || this._saving} | ||||
|                 @click=${this._save} | ||||
|               > | ||||
|                 ${this.hass!.localize( | ||||
|                   !this._cardEditorEl || this._GUImode | ||||
|                     ? "ui.panel.lovelace.editor.edit_card.show_code_editor" | ||||
|                     : "ui.panel.lovelace.editor.edit_card.show_visual_editor" | ||||
|                 )} | ||||
|               </mwc-button> | ||||
|                 ${this._saving | ||||
|                   ? html` | ||||
|                       <ha-circular-progress | ||||
|                         indeterminate | ||||
|                         aria-label="Saving" | ||||
|                         size="small" | ||||
|                       ></ha-circular-progress> | ||||
|                     ` | ||||
|                   : this.hass!.localize("ui.common.save")} | ||||
|               </ha-button> | ||||
|             ` | ||||
|           : ""} | ||||
|         <div slot="primaryAction" @click=${this._save}> | ||||
|           <mwc-button @click=${this._cancel} dialogInitialFocus> | ||||
|             ${this.hass!.localize("ui.common.cancel")} | ||||
|           </mwc-button> | ||||
|           ${this._cardConfig !== undefined && this._dirty | ||||
|             ? html` | ||||
|                 <mwc-button | ||||
|                   ?disabled=${!this._canSave || this._saving} | ||||
|                   @click=${this._save} | ||||
|                 > | ||||
|                   ${this._saving | ||||
|                     ? html` | ||||
|                         <ha-circular-progress | ||||
|                           indeterminate | ||||
|                           aria-label="Saving" | ||||
|                           size="small" | ||||
|                         ></ha-circular-progress> | ||||
|                       ` | ||||
|                     : this.hass!.localize("ui.common.save")} | ||||
|                 </mwc-button> | ||||
|               ` | ||||
|             : ``} | ||||
|         </div> | ||||
|           : nothing} | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _enableGuiMode() { | ||||
|     this._yamlMode = false; | ||||
|   } | ||||
|  | ||||
|   private _enableYamlMode() { | ||||
|     this._yamlMode = true; | ||||
|   } | ||||
|  | ||||
|   private _enlarge() { | ||||
|     this.large = !this.large; | ||||
|   } | ||||
| @@ -328,27 +376,21 @@ export class HuiDialogEditCard | ||||
|     ev.stopPropagation(); | ||||
|   } | ||||
|  | ||||
|   private _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) { | ||||
|     this._cardConfig = deepFreeze(ev.detail.config); | ||||
|     this._error = ev.detail.error; | ||||
|     this._guiModeAvailable = ev.detail.guiModeAvailable; | ||||
|   private _handleYAMLChanged(ev: CustomEvent) { | ||||
|     this._cardConfig = load(ev.detail.value) as LovelaceCardConfig; | ||||
|     this._dirty = true; | ||||
|   } | ||||
|  | ||||
|   private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void { | ||||
|     ev.stopPropagation(); | ||||
|     this._GUImode = ev.detail.guiMode; | ||||
|     this._guiModeAvailable = ev.detail.guiModeAvailable; | ||||
|   } | ||||
|  | ||||
|   private _toggleMode(): void { | ||||
|     this._cardEditorEl?.toggleMode(); | ||||
|   private _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) { | ||||
|     this._cardConfig = deepFreeze(ev.detail.config); | ||||
|     this._error = ev.detail.error; | ||||
|     this._dirty = true; | ||||
|   } | ||||
|  | ||||
|   private _opened() { | ||||
|     window.addEventListener("dialog-closed", this._enableEscapeKeyClose); | ||||
|     window.addEventListener("hass-more-info", this._disableEscapeKeyClose); | ||||
|     this._cardEditorEl?.focusYamlEditor(); | ||||
|     // this._cardEditorEl?.focusYamlEditor(); | ||||
|   } | ||||
|  | ||||
|   private get _isInSection() { | ||||
| @@ -551,11 +593,6 @@ export class HuiDialogEditCard | ||||
|           width: 100%; | ||||
|           box-sizing: border-box; | ||||
|         } | ||||
|         .gui-mode-button { | ||||
|           margin-right: auto; | ||||
|           margin-inline-end: auto; | ||||
|           margin-inline-start: initial; | ||||
|         } | ||||
|         .header { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
| @@ -565,6 +602,12 @@ export class HuiDialogEditCard | ||||
|           color: inherit; | ||||
|           text-decoration: none; | ||||
|         } | ||||
|         .selected_menu_item { | ||||
|           color: var(--primary-color); | ||||
|         } | ||||
|         .blank-icon { | ||||
|           width: 16px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import "@material/mwc-button"; | ||||
| import { mdiCodeBraces, mdiListBox } from "@mdi/js"; | ||||
| import { dump, load } from "js-yaml"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
| @@ -7,6 +7,7 @@ import { | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { property, query, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| @@ -16,14 +17,17 @@ import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-circular-progress"; | ||||
| import "../../../components/ha-code-editor"; | ||||
| import type { HaCodeEditor } from "../../../components/ha-code-editor"; | ||||
| import "../../../components/ha-outlined-segmented-button"; | ||||
| import "../../../components/ha-outlined-segmented-button-set"; | ||||
| import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | ||||
| import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy"; | ||||
| import { LovelaceConfig } from "../../../data/lovelace/config/types"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { LovelaceCardFeatureConfig } from "../card-features/types"; | ||||
| import { LovelaceElementConfig } from "../elements/types"; | ||||
| import type { LovelaceRowConfig } from "../entity-rows/types"; | ||||
| import { LovelaceHeaderFooterConfig } from "../header-footer/types"; | ||||
| import { LovelaceElementConfig } from "../elements/types"; | ||||
| import type { | ||||
|   LovelaceConfigForm, | ||||
|   LovelaceGenericElementEditor, | ||||
| @@ -33,7 +37,6 @@ import type { HuiFormEditor } from "./config-elements/hui-form-editor"; | ||||
| import "./config-elements/hui-generic-entity-row-editor"; | ||||
| import { GUISupportError } from "./gui-support-error"; | ||||
| import { EditSubElementEvent, GUIModeChangedEvent } from "./types"; | ||||
| import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | ||||
|  | ||||
| export interface ConfigChangedEvent { | ||||
|   config: | ||||
| @@ -73,6 +76,9 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public context?: C; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "show-toggle-mode-button" }) | ||||
|   public showToggleModeButton = false; | ||||
|  | ||||
|   @state() private _yaml?: string; | ||||
|  | ||||
|   @state() private _config?: T; | ||||
| @@ -208,8 +214,40 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const guiModeAvailable = !( | ||||
|       this.hasWarning || | ||||
|       this.hasError || | ||||
|       this._guiSupported === false | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <div class="wrapper"> | ||||
|         ${this.showToggleModeButton | ||||
|           ? html` | ||||
|               <div class="header"> | ||||
|                 <ha-outlined-segmented-button-set | ||||
|                   @segmented-button-set-selection=${this._handleModeSelected} | ||||
|                 > | ||||
|                   <ha-outlined-segmented-button | ||||
|                     .selected=${this._guiMode} | ||||
|                     .disabled=${!guiModeAvailable} | ||||
|                     no-checkmark | ||||
|                   > | ||||
|                     <ha-svg-icon slot="icon" .path=${mdiListBox}></ha-svg-icon> | ||||
|                   </ha-outlined-segmented-button> | ||||
|                   <ha-outlined-segmented-button | ||||
|                     .selected=${!this._guiMode} | ||||
|                     no-checkmark | ||||
|                   > | ||||
|                     <ha-svg-icon | ||||
|                       slot="icon" | ||||
|                       .path=${mdiCodeBraces} | ||||
|                     ></ha-svg-icon> | ||||
|                   </ha-outlined-segmented-button> | ||||
|                 </ha-outlined-segmented-button-set> | ||||
|               </div> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${this.GUImode | ||||
|           ? html` | ||||
|               <div class="gui-editor"> | ||||
| @@ -241,43 +279,58 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement { | ||||
|             `} | ||||
|         ${this._guiSupported === false && this.configElementType | ||||
|           ? html` | ||||
|               <div class="info"> | ||||
|                 ${this.hass.localize("ui.errors.config.editor_not_available", { | ||||
|                   type: this.configElementType, | ||||
|                 })} | ||||
|               </div> | ||||
|               <ha-alert | ||||
|                 alert-type="info" | ||||
|                 .title=${this.hass.localize( | ||||
|                   "ui.errors.config.editor_not_supported" | ||||
|                 )} | ||||
|               > | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.errors.config.editor_not_supported_details", | ||||
|                   { type: this.configElementType } | ||||
|                 )} | ||||
|                 <br /> | ||||
|                 ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} | ||||
|               </ha-alert> | ||||
|             ` | ||||
|           : ""} | ||||
|           : nothing} | ||||
|         ${this.hasError | ||||
|           ? html` | ||||
|               <div class="error"> | ||||
|                 ${this.hass.localize("ui.errors.config.error_detected")}: | ||||
|                 <br /> | ||||
|               <ha-alert | ||||
|                 alert-type="error" | ||||
|                 .title=${this.hass.localize( | ||||
|                   "ui.errors.config.invalid_configuration" | ||||
|                 )} | ||||
|               > | ||||
|                 ${this.hass.localize("ui.errors.config.error_details")} | ||||
|                 <ul> | ||||
|                   ${this._errors!.map((error) => html`<li>${error}</li>`)} | ||||
|                 </ul> | ||||
|               </div> | ||||
|               </ha-alert> | ||||
|             ` | ||||
|           : ""} | ||||
|           : nothing} | ||||
|         ${this.hasWarning | ||||
|           ? html` | ||||
|               <ha-alert | ||||
|                 alert-type="warning" | ||||
|                 .title="${this.hass.localize( | ||||
|                 .title=${this.hass.localize( | ||||
|                   "ui.errors.config.editor_not_supported" | ||||
|                 )}:" | ||||
|                 )} | ||||
|               > | ||||
|                 ${this._warnings!.length > 0 && this._warnings![0] !== undefined | ||||
|                   ? html` <ul> | ||||
|                       ${this._warnings!.map( | ||||
|                         (warning) => html`<li>${warning}</li>` | ||||
|                       )} | ||||
|                     </ul>` | ||||
|                   : ""} | ||||
|                   ? html` | ||||
|                       ${this.hass.localize("ui.errors.config.warning_details")} | ||||
|                       <ul> | ||||
|                         ${this._warnings!.map( | ||||
|                           (warning) => html`<li>${warning}</li>` | ||||
|                         )} | ||||
|                       </ul> | ||||
|                     ` | ||||
|                   : nothing} | ||||
|                 ${this.hass.localize("ui.errors.config.edit_in_yaml_supported")} | ||||
|               </ha-alert> | ||||
|             ` | ||||
|           : ""} | ||||
|           : nothing} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
| @@ -311,6 +364,10 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement { | ||||
|     this.value = config as unknown as T; | ||||
|   } | ||||
|  | ||||
|   private _handleModeSelected(ev) { | ||||
|     this.GUImode = ev.detail.index === 0; | ||||
|   } | ||||
|  | ||||
|   private _handleYAMLChanged(ev: CustomEvent) { | ||||
|     ev.stopPropagation(); | ||||
|     const newYaml = ev.detail.value; | ||||
| @@ -452,6 +509,10 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement { | ||||
|         display: block; | ||||
|         margin: auto; | ||||
|       } | ||||
|       .header { | ||||
|         display: flex; | ||||
|         justify-content: flex-end; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1797,10 +1797,13 @@ | ||||
|     "errors": { | ||||
|       "config": { | ||||
|         "no_type_provided": "No type provided.", | ||||
|         "error_detected": "Configuration errors detected", | ||||
|         "invalid_configuration": "Invalid configuration", | ||||
|         "error_details": "The configuration contains the following errors:", | ||||
|         "editor_not_available": "No visual editor available for type ''{type}''.", | ||||
|         "editor_not_supported": "Visual editor is not supported for this configuration", | ||||
|         "edit_in_yaml_supported": "You can still edit your config in YAML.", | ||||
|         "editor_not_supported": "Visual editor not supported", | ||||
|         "editor_not_supported_details": "The visual editor is not supported for type ''{type}''.", | ||||
|         "warning_details": "The configuration contains the following warnings:", | ||||
|         "edit_in_yaml_supported": "You can still edit the configuration in YAML.", | ||||
|         "key_missing": "Required key ''{key}'' is missing.", | ||||
|         "key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.", | ||||
|         "key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user