Add code mirror editors to the dev-tools data fields (#3981)

* Add yaml code mirror editor to the dev-tools yaml fields

* Add jinja2 editor on dev template

* Migrate to UpdatingElement, review comments

* update cm, add types

* types

* dev tools mqtt
pull/3995/head
Bram Kragten 2019-10-12 21:33:51 +02:00 committed by Paulus Schoutsen
parent 4728c12225
commit 12840231be
15 changed files with 387 additions and 265 deletions

View File

@ -74,7 +74,7 @@
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0", "chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0", "chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.45.0", "codemirror": "^5.49.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"es6-object-assign": "^1.1.0", "es6-object-assign": "^1.1.0",
@ -117,6 +117,7 @@
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1", "@types/chromecast-caf-sender": "^1.0.1",
"@types/codemirror": "^0.0.78",
"@types/hls.js": "^0.12.3", "@types/hls.js": "^0.12.3",
"@types/leaflet": "^1.4.3", "@types/leaflet": "^1.4.3",
"@types/memoize-one": "4.1.0", "@types/memoize-one": "4.1.0",

View File

@ -0,0 +1,160 @@
import { loadCodeMirror } from "../resources/codemirror.ondemand";
import { fireEvent } from "../common/dom/fire_event";
import {
UpdatingElement,
property,
customElement,
PropertyValues,
} from "lit-element";
import { Editor } from "codemirror";
declare global {
interface HASSDomEvents {
"editor-save": undefined;
}
}
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: Editor;
@property() public mode?: string;
@property() public autofocus = false;
@property() public rtl = false;
@property() public error = false;
@property() private _value = "";
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.getValue() : this._value;
}
public get hasComments(): boolean {
return this.shadowRoot!.querySelector("span.cm-comment") ? true : false;
}
public connectedCallback() {
super.connectedCallback();
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
}
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
if (!this.codemirror) {
return;
}
if (changedProps.has("mode")) {
this.codemirror.setOption("mode", this.mode);
}
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._load();
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
const codeMirror = loaded.codeMirror;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
:host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _onChange(): void {
const newValue = this.value;
if (newValue === this._value) {
return;
}
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-code-editor": HaCodeEditor;
}
}

View File

@ -1,139 +0,0 @@
// @ts-ignore
import CodeMirror from "codemirror";
import "codemirror/mode/yaml/yaml";
// @ts-ignore
import codeMirrorCSS from "codemirror/lib/codemirror.css";
import { fireEvent } from "../common/dom/fire_event";
import { customElement } from "lit-element";
declare global {
interface HASSDomEvents {
"yaml-changed": {
value: string;
};
"yaml-save": undefined;
}
}
@customElement("ha-yaml-editor")
export class HaYamlEditor extends HTMLElement {
public codemirror?: any;
private _autofocus = false;
private _rtl = false;
private _value: string;
public constructor() {
super();
CodeMirror.commands.save = (cm: CodeMirror) => {
fireEvent(cm.getWrapperElement(), "yaml-save");
};
this._value = "";
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
${codeMirrorCSS}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
:host(.error-state) .CodeMirror-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--primary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
</style>`;
}
set value(value: string) {
if (this.codemirror) {
if (value !== this.codemirror.getValue()) {
this.codemirror.setValue(value);
}
}
this._value = value;
}
get value(): string {
return this.codemirror.getValue();
}
set rtl(rtl: boolean) {
this._rtl = rtl;
this.setScrollBarDirection();
}
set autofocus(autofocus: boolean) {
this._autofocus = autofocus;
if (this.codemirror) {
this.codemirror.focus();
}
}
set error(error: boolean) {
this.classList.toggle("error-state", error);
}
get hasComments(): boolean {
return this.shadowRoot!.querySelector("span.cm-comment") ? true : false;
}
public connectedCallback(): void {
if (!this.codemirror) {
this.codemirror = CodeMirror(
(this.shadowRoot as unknown) as HTMLElement,
{
value: this._value,
lineNumbers: true,
mode: "yaml",
tabSize: 2,
autofocus: this._autofocus,
viewportMargin: Infinity,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [],
}
);
this.setScrollBarDirection();
this.codemirror.on("changes", () => this._onChange());
} else {
this.codemirror.refresh();
}
}
private _onChange(): void {
fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() });
}
private setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this._rtl);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-yaml-editor": HaYamlEditor;
}
}

View File

@ -20,7 +20,7 @@ declare global {
"ha-device-picker": any; "ha-device-picker": any;
"ha-device-condition-picker": any; "ha-device-condition-picker": any;
"ha-textarea": any; "ha-textarea": any;
"ha-yaml-editor": any; "ha-code-editor": any;
"ha-service-picker": any; "ha-service-picker": any;
"mwc-button": any; "mwc-button": any;
"ha-device-trigger-picker": any; "ha-device-trigger-picker": any;

View File

@ -1,8 +1,6 @@
import { h, Component } from "preact"; import { h, Component } from "preact";
import yaml from "js-yaml"; import yaml from "js-yaml";
import "../../../components/ha-yaml-editor"; import "../../../components/ha-code-editor";
// tslint:disable-next-line
import { HaYamlEditor } from "../../../components/ha-yaml-editor";
const isEmpty = (obj: object) => { const isEmpty = (obj: object) => {
for (const key in obj) { for (const key in obj) {
@ -14,8 +12,6 @@ const isEmpty = (obj: object) => {
}; };
export default class YAMLTextArea extends Component<any, any> { export default class YAMLTextArea extends Component<any, any> {
private _yamlEditor!: HaYamlEditor;
constructor(props) { constructor(props) {
super(props); super(props);
@ -63,12 +59,6 @@ export default class YAMLTextArea extends Component<any, any> {
} }
} }
public componentDidMount() {
setTimeout(() => {
this._yamlEditor.codemirror.refresh();
}, 1);
}
public render({ label }, { value, isValid }) { public render({ label }, { value, isValid }) {
const style: any = { const style: any = {
minWidth: 300, minWidth: 300,
@ -77,16 +67,14 @@ export default class YAMLTextArea extends Component<any, any> {
return ( return (
<div> <div>
<p>{label}</p> <p>{label}</p>
<ha-yaml-editor <ha-code-editor
ref={this._storeYamlEditorRef} mode="yaml"
style={style} style={style}
value={value} value={value}
error={isValid === false} error={isValid === false}
onyaml-changed={this.onChange} onvalue-changed={this.onChange}
/> />
</div> </div>
); );
} }
private _storeYamlEditorRef = (yamlEditor) => (this._yamlEditor = yamlEditor);
} }

View File

@ -1,17 +1,18 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml"; import yaml from "js-yaml";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import "./events-list"; import "./events-list";
import "./event-subscribe-card"; import "./event-subscribe-card";
import { EventsMixin } from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
const ERROR_SENTINEL = {};
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
@ -32,6 +33,11 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
.ha-form { .ha-form {
margin-right: 16px; margin-right: 16px;
max-width: 400px;
}
mwc-button {
margin-top: 8px;
} }
.header { .header {
@ -62,11 +68,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
required required
value="{{eventType}}" value="{{eventType}}"
></paper-input> ></paper-input>
<paper-textarea <p>Event Data (YAML, optional)</p>
label="Event Data (YAML, optional)" <ha-code-editor
value="{{eventData}}" mode="yaml"
></paper-textarea> value="[[eventData]]"
<mwc-button on-click="fireEvent" raised>Fire Event</mwc-button> error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<mwc-button on-click="fireEvent" raised disabled="[[!validJSON]]"
>Fire Event</mwc-button
>
</div> </div>
</div> </div>
@ -97,6 +108,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
type: String, type: String,
value: "", value: "",
}, },
parsedJSON: {
type: Object,
computed: "_computeParsedEventData(eventData)",
},
validJSON: {
type: Boolean,
computed: "_computeValidJSON(parsedJSON)",
},
}; };
} }
@ -104,19 +125,28 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) {
this.eventType = ev.detail.eventType; this.eventType = ev.detail.eventType;
} }
fireEvent() { _computeParsedEventData(eventData) {
var eventData;
try { try {
eventData = this.eventData ? yaml.safeLoad(this.eventData) : {}; return eventData.trim() ? yaml.safeLoad(eventData) : {};
} catch (err) { } catch (err) {
/* eslint-disable no-alert */ return ERROR_SENTINEL;
alert("Error parsing YAML: " + err); }
/* eslint-enable no-alert */ }
_computeValidJSON(parsedJSON) {
return parsedJSON !== ERROR_SENTINEL;
}
_yamlChanged(ev) {
this.eventData = ev.detail.value;
}
fireEvent() {
if (!this.eventType) {
alert("Event type is a mandatory field");
return; return;
} }
this.hass.callApi("POST", "events/" + this.eventType, this.parsedJSON).then(
this.hass.callApi("POST", "events/" + this.eventType, eventData).then(
function() { function() {
this.fire("hass-notification", { this.fire("hass-notification", {
message: "Event " + this.eventType + " successful fired!", message: "Event " + this.eventType + " successful fired!",

View File

@ -9,12 +9,12 @@ import {
} from "lit-element"; } from "lit-element";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-code-editor";
import "./mqtt-subscribe-card"; import "./mqtt-subscribe-card";
@customElement("developer-tools-mqtt") @customElement("developer-tools-mqtt")
@ -48,12 +48,12 @@ class HaPanelDevMqtt extends LitElement {
@value-changed=${this._handleTopic} @value-changed=${this._handleTopic}
></paper-input> ></paper-input>
<paper-textarea <p>Payload (template allowed)</p>
always-float-label <ha-code-editor
label="Payload (template allowed)" mode="jinja2"
.value="${this.payload}" .value="${this.payload}"
@value-changed=${this._handlePayload} @value-changed=${this._handlePayload}
></paper-textarea> ></ha-code-editor>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._publish}>Publish</mwc-button> <mwc-button @click=${this._publish}>Publish</mwc-button>

View File

@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
@ -7,6 +6,7 @@ import yaml from "js-yaml";
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor";
import "../../../components/ha-service-picker"; import "../../../components/ha-service-picker";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import "../../../util/app-localstorage-document"; import "../../../util/app-localstorage-document";
@ -30,6 +30,10 @@ class HaPanelDevService extends PolymerElement {
max-width: 400px; max-width: 400px;
} }
mwc-button {
margin-top: 8px;
}
.description { .description {
margin-top: 24px; margin-top: 24px;
white-space: pre-wrap; white-space: pre-wrap;
@ -109,20 +113,16 @@ class HaPanelDevService extends PolymerElement {
allow-custom-entity allow-custom-entity
></ha-entity-picker> ></ha-entity-picker>
</template> </template>
<paper-textarea <p>Service Data (YAML, optional)</p>
always-float-label <ha-code-editor
label="Service Data (YAML, optional)" mode="yaml"
value="{{serviceData}}" value="[[serviceData]]"
autocapitalize="none" error="[[!validJSON]]"
autocomplete="off" on-value-changed="_yamlChanged"
spellcheck="false" ></ha-code-editor>
></paper-textarea>
<mwc-button on-click="_callService" raised disabled="[[!validJSON]]"> <mwc-button on-click="_callService" raised disabled="[[!validJSON]]">
Call Service Call Service
</mwc-button> </mwc-button>
<template is="dom-if" if="[[!validJSON]]">
<span class="error">Invalid YAML</span>
</template>
</div> </div>
<template is="dom-if" if="[[!domainService]]"> <template is="dom-if" if="[[!domainService]]">
@ -305,6 +305,10 @@ class HaPanelDevService extends PolymerElement {
entity_id: ev.target.value, entity_id: ev.target.value,
}); });
} }
_yamlChanged(ev) {
this.serviceData = ev.detail.value;
}
} }
customElements.define("developer-tools-service", HaPanelDevService); customElements.define("developer-tools-service", HaPanelDevService);

View File

@ -1,16 +1,17 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-input/paper-textarea";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import yaml from "js-yaml"; import yaml from "js-yaml";
import "../../../components/entity/ha-entity-picker"; import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import { EventsMixin } from "../../../mixins/events-mixin"; import { EventsMixin } from "../../../mixins/events-mixin";
const ERROR_SENTINEL = {};
/* /*
* @appliesMixin EventsMixin * @appliesMixin EventsMixin
*/ */
@ -27,13 +28,14 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
direction: ltr; direction: ltr;
} }
ha-entity-picker, .inputs {
.state-input,
paper-textarea {
display: block;
max-width: 400px; max-width: 400px;
} }
mwc-button {
margin-top: 8px;
}
.entities th { .entities th {
text-align: left; text-align: left;
} }
@ -66,7 +68,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
} }
</style> </style>
<div> <div class="inputs">
<p> <p>
Set the representation of a device within Home Assistant.<br /> Set the representation of a device within Home Assistant.<br />
This will not communicate with the actual device. This will not communicate with the actual device.
@ -89,14 +91,16 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
value="{{_state}}" value="{{_state}}"
class="state-input" class="state-input"
></paper-input> ></paper-input>
<paper-textarea <p>State attributes (YAML, optional)</p>
label="State attributes (YAML, optional)" <ha-code-editor
autocapitalize="none" mode="yaml"
autocomplete="off" value="[[_stateAttributes]]"
spellcheck="false" error="[[!validJSON]]"
value="{{_stateAttributes}}" on-value-changed="_yamlChanged"
></paper-textarea> ></ha-code-editor>
<mwc-button on-click="handleSetState" raised>Set State</mwc-button> <mwc-button on-click="handleSetState" disabled="[[!validJSON]]" raised
>Set State</mwc-button
>
</div> </div>
<h1>Current entities</h1> <h1>Current entities</h1>
@ -166,6 +170,16 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
type: Object, type: Object,
}, },
parsedJSON: {
type: Object,
computed: "_computeParsedStateAttributes(_stateAttributes)",
},
validJSON: {
type: Boolean,
computed: "_computeValidJSON(parsedJSON)",
},
_entityId: { _entityId: {
type: String, type: String,
value: "", value: "",
@ -229,20 +243,13 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
} }
handleSetState() { handleSetState() {
var attr; if (!this._entityId) {
alert("Entity is a mandatory field");
try {
attr = this._stateAttributes ? yaml.safeLoad(this._stateAttributes) : {};
} catch (err) {
/* eslint-disable no-alert */
alert("Error parsing YAML: " + err);
/* eslint-enable no-alert */
return; return;
} }
this.hass.callApi("POST", "states/" + this._entityId, { this.hass.callApi("POST", "states/" + this._entityId, {
state: this._state, state: this._state,
attributes: attr, attributes: this.parsedJSON,
}); });
} }
@ -341,6 +348,22 @@ class HaPanelDevState extends EventsMixin(PolymerElement) {
return output; return output;
} }
_computeParsedStateAttributes(stateAttributes) {
try {
return stateAttributes.trim() ? yaml.safeLoad(stateAttributes) : {};
} catch (err) {
return ERROR_SENTINEL;
}
}
_computeValidJSON(parsedJSON) {
return parsedJSON !== ERROR_SENTINEL;
}
_yamlChanged(ev) {
this._stateAttributes = ev.detail.value;
}
} }
customElements.define("developer-tools-state", HaPanelDevState); customElements.define("developer-tools-state", HaPanelDevState);

View File

@ -1,9 +1,9 @@
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-spinner/paper-spinner"; import "@polymer/paper-spinner/paper-spinner";
import { timeOut } from "@polymer/polymer/lib/utils/async"; import { timeOut } from "@polymer/polymer/lib/utils/async";
import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../components/ha-code-editor";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
@ -46,12 +46,6 @@ class HaPanelDevTemplate extends PolymerElement {
right: 8px; right: 8px;
} }
paper-textarea {
--paper-input-container-input: {
@apply --paper-font-code1;
}
}
.rendered { .rendered {
@apply --paper-font-code1; @apply --paper-font-code1;
clear: both; clear: both;
@ -85,11 +79,14 @@ class HaPanelDevTemplate extends PolymerElement {
> >
</li> </li>
</ul> </ul>
<paper-textarea <p>Template editor</p>
label="Template editor" <ha-code-editor
value="{{template}}" mode="jinja2"
value="[[template]]"
error="[[error]]"
autofocus autofocus
></paper-textarea> on-value-changed="templateChanged"
></ha-code-editor>
</div> </div>
<div class="render-pane"> <div class="render-pane">
@ -144,7 +141,6 @@ For loop example:
{{ state.name | lower }} is {{state.state_with_unit}} {{ state.name | lower }} is {{state.state_with_unit}}
{%- endfor %}.`, {%- endfor %}.`,
/* eslint-enable max-len */ /* eslint-enable max-len */
observer: "templateChanged",
}, },
processed: { processed: {
@ -154,6 +150,11 @@ For loop example:
}; };
} }
ready() {
super.ready();
this.renderTemplate();
}
computeFormClasses(narrow) { computeFormClasses(narrow) {
return narrow ? "content fit" : "content fit layout horizontal"; return narrow ? "content fit" : "content fit layout horizontal";
} }
@ -162,7 +163,8 @@ For loop example:
return error ? "error rendered" : "rendered"; return error ? "error rendered" : "rendered";
} }
templateChanged() { templateChanged(ev) {
this.template = ev.detail.value;
if (this.error) { if (this.error) {
this.error = false; this.error = false;
} }

View File

@ -17,10 +17,10 @@ import { LovelaceCardEditor } from "../../types";
import { getCardElementTag } from "../../common/get-card-element-tag"; import { getCardElementTag } from "../../common/get-card-element-tag";
import { computeRTL } from "../../../../common/util/compute_rtl"; import { computeRTL } from "../../../../common/util/compute_rtl";
import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-code-editor";
// This is not a duplicate import, one is for types, one is for element. // This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line // tslint:disable-next-line
import { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import { HaCodeEditor } from "../../../../components/ha-code-editor";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { EntityConfig } from "../../entity-rows/types"; import { EntityConfig } from "../../entity-rows/types";
@ -65,12 +65,6 @@ export class HuiCardEditor extends LitElement {
try { try {
this._config = yaml.safeLoad(this.yaml); this._config = yaml.safeLoad(this.yaml);
this._updateConfigElement(); this._updateConfigElement();
setTimeout(() => {
if (this._yamlEditor) {
this._yamlEditor.codemirror.refresh();
}
fireEvent(this as HTMLElement, "iron-resize");
}, 1);
this._error = undefined; this._error = undefined;
} catch (err) { } catch (err) {
this._error = err.message; this._error = err.message;
@ -94,14 +88,19 @@ export class HuiCardEditor extends LitElement {
return this._error !== undefined; return this._error !== undefined;
} }
private get _yamlEditor(): HaYamlEditor { private get _yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("ha-yaml-editor")!; return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
} }
public toggleMode() { public toggleMode() {
this._GUImode = !this._GUImode; this._GUImode = !this._GUImode;
} }
public connectedCallback() {
super.connectedCallback();
this._refreshYamlEditor();
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="wrapper"> <div class="wrapper">
@ -121,12 +120,14 @@ export class HuiCardEditor extends LitElement {
` `
: html` : html`
<div class="yaml-editor"> <div class="yaml-editor">
<ha-yaml-editor <ha-code-editor
.autofocus=${true} mode="yaml"
.rtl=${computeRTL(this.hass)} autofocus
.value=${this.yaml} .value=${this.yaml}
@yaml-changed=${this._handleYAMLChanged} .error=${this._error}
></ha-yaml-editor> .rtl=${computeRTL(this.hass)}
@value-changed=${this._handleYAMLChanged}
></ha-code-editor>
</div> </div>
`} `}
${this._error ${this._error
@ -165,13 +166,25 @@ export class HuiCardEditor extends LitElement {
if (changedProperties.has("_GUImode")) { if (changedProperties.has("_GUImode")) {
if (this._GUImode === false) { if (this._GUImode === false) {
// Refresh code editor when switching to yaml mode // Refresh code editor when switching to yaml mode
this._yamlEditor.codemirror.refresh(); this._refreshYamlEditor(true);
this._yamlEditor.codemirror.focus();
} }
fireEvent(this as HTMLElement, "iron-resize"); fireEvent(this as HTMLElement, "iron-resize");
} }
} }
private _refreshYamlEditor(focus = false) {
// wait on render
setTimeout(() => {
if (this._yamlEditor && this._yamlEditor.codemirror) {
this._yamlEditor.codemirror.refresh();
if (focus) {
this._yamlEditor.codemirror.focus();
}
}
fireEvent(this as HTMLElement, "iron-resize");
}, 1);
}
private _handleUIConfigChanged(ev: UIConfigChangedEvent) { private _handleUIConfigChanged(ev: UIConfigChangedEvent) {
ev.stopPropagation(); ev.stopPropagation();
const config = ev.detail.config; const config = ev.detail.config;

View File

@ -14,10 +14,10 @@ import { Lovelace } from "./types";
import "../../components/ha-icon"; import "../../components/ha-icon";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import "../../components/ha-yaml-editor"; import "../../components/ha-code-editor";
// This is not a duplicate import, one is for types, one is for element. // This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line // tslint:disable-next-line
import { HaYamlEditor } from "../../components/ha-yaml-editor"; import { HaCodeEditor } from "../../components/ha-code-editor";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
@ -33,7 +33,7 @@ class LovelaceFullConfigEditor extends LitElement {
public closeEditor?: () => void; public closeEditor?: () => void;
private _saving?: boolean; private _saving?: boolean;
private _changed?: boolean; private _changed?: boolean;
private _generation?: number; private _generation = 1;
static get properties() { static get properties() {
return { return {
@ -81,14 +81,15 @@ class LovelaceFullConfigEditor extends LitElement {
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class="content"> <div class="content">
<ha-yaml-editor <ha-code-editor
.autofocus=${true} mode="yaml"
autofocus
.rtl=${computeRTL(this.hass)} .rtl=${computeRTL(this.hass)}
.hass="${this.hass}" .hass="${this.hass}"
@yaml-changed="${this._yamlChanged}" @value-changed="${this._yamlChanged}"
@yaml-save="${this._handleSave}" @editor-save="${this._handleSave}"
> >
</ha-yaml-editor> </ha-code-editor>
</div> </div>
</app-header-layout> </app-header-layout>
`; `;
@ -96,8 +97,6 @@ class LovelaceFullConfigEditor extends LitElement {
protected firstUpdated() { protected firstUpdated() {
this.yamlEditor.value = yaml.safeDump(this.lovelace!.config); this.yamlEditor.value = yaml.safeDump(this.lovelace!.config);
this.yamlEditor.codemirror.clearHistory();
this._generation = this.yamlEditor.codemirror.changeGeneration(true);
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -143,10 +142,9 @@ class LovelaceFullConfigEditor extends LitElement {
} }
private _yamlChanged() { private _yamlChanged() {
if (!this._generation) { this._changed = !this.yamlEditor
return; .codemirror!.getDoc()
} .isClean(this._generation);
this._changed = !this.yamlEditor.codemirror.isClean(this._generation);
if (this._changed && !window.onbeforeunload) { if (this._changed && !window.onbeforeunload) {
window.onbeforeunload = () => { window.onbeforeunload = () => {
return true; return true;
@ -202,14 +200,16 @@ class LovelaceFullConfigEditor extends LitElement {
} catch (err) { } catch (err) {
alert(`Unable to save YAML: ${err}`); alert(`Unable to save YAML: ${err}`);
} }
this._generation = this.yamlEditor.codemirror.changeGeneration(true); this._generation = this.yamlEditor
.codemirror!.getDoc()
.changeGeneration(true);
window.onbeforeunload = null; window.onbeforeunload = null;
this._saving = false; this._saving = false;
this._changed = false; this._changed = false;
} }
private get yamlEditor(): HaYamlEditor { private get yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("ha-yaml-editor")!; return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
} }
} }

View File

@ -0,0 +1,13 @@
interface LoadedCodeMirror {
codeMirror: any;
codeMirrorCss: any;
}
let loaded: Promise<LoadedCodeMirror>;
export const loadCodeMirror = async (): Promise<LoadedCodeMirror> => {
if (!loaded) {
loaded = import(/* webpackChunkName: "codemirror" */ "./codemirror");
}
return loaded;
};

View File

@ -0,0 +1,13 @@
// @ts-ignore
import _CodeMirror, { Editor } from "codemirror";
// @ts-ignore
import _codeMirrorCss from "codemirror/lib/codemirror.css";
import "codemirror/mode/yaml/yaml";
import "codemirror/mode/jinja2/jinja2";
import { fireEvent } from "../common/dom/fire_event";
_CodeMirror.commands.save = (cm: Editor) => {
fireEvent(cm.getWrapperElement(), "editor-save");
};
export const codeMirror: any = _CodeMirror;
export const codeMirrorCss: any = _codeMirrorCss;

View File

@ -1652,6 +1652,13 @@
resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614" resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614"
integrity sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ= integrity sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=
"@types/codemirror@^0.0.78":
version "0.0.78"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.78.tgz#75a8eabda268c8e734855fb24e8c86192e2e18ad"
integrity sha512-QpMQUpEL+ZNcpEhjvYM/H6jqDx9nNcJqymA2kbkNthFS2I7ekL7ofEZ7+MoQAFTBuJers91K0FGCMpL7MwC9TQ==
dependencies:
"@types/tern" "*"
"@types/compression@^0.0.33": "@types/compression@^0.0.33":
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.33.tgz#95dc733a2339aa846381d7f1377792d2553dc27d" resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.33.tgz#95dc733a2339aa846381d7f1377792d2553dc27d"
@ -1686,7 +1693,7 @@
resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a"
integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw== integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==
"@types/estree@0.0.39": "@types/estree@*", "@types/estree@0.0.39":
version "0.0.39" version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
@ -1894,6 +1901,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/tern@*":
version "0.23.3"
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
integrity sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==
dependencies:
"@types/estree" "*"
"@types/ua-parser-js@^0.7.31": "@types/ua-parser-js@^0.7.31":
version "0.7.32" version "0.7.32"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.32.tgz#8827d451d6702307248073b5d98aa9293d02b5e5" resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.32.tgz#8827d451d6702307248073b5d98aa9293d02b5e5"
@ -4041,10 +4055,10 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror@^5.45.0: codemirror@^5.49.0:
version "5.45.0" version "5.49.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.45.0.tgz#db5ebbb3bf44028c684053f3954d011efcec27ad" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.49.0.tgz#adedbffcc81091e4a0334bcb96b1ae3b7ada5e3f"
integrity sha512-c19j644usCE8gQaXa0jqn2B/HN9MnB2u6qPIrrhrMkB+QAP42y8G4QnTwuwbVSoUS1jEl7JU9HZMGhCDL0nsAw== integrity sha512-Hyzr0HToBdZpLBN9dYFO/KlJAsKH37/cXVHPAqa+imml0R92tb9AkmsvjnXL+SluEvjjdfkDgRjc65NG5jnMYA==
collection-map@^1.0.0: collection-map@^1.0.0:
version "1.0.0" version "1.0.0"