mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 11:19:25 +00:00
Compare commits
8 Commits
20241002.1
...
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