diff --git a/package.json b/package.json index c54c656c04..00ead71952 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@webcomponents/webcomponentsjs": "^2.2.7", "chart.js": "~2.8.0", "chartjs-chart-timeline": "^0.3.0", - "codemirror": "^5.45.0", + "codemirror": "^5.49.0", "cpx": "^1.5.0", "deep-clone-simple": "^1.1.1", "es6-object-assign": "^1.1.0", @@ -117,6 +117,7 @@ "@types/chai": "^4.1.7", "@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-sender": "^1.0.1", + "@types/codemirror": "^0.0.78", "@types/hls.js": "^0.12.3", "@types/leaflet": "^1.4.3", "@types/memoize-one": "4.1.0", diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts new file mode 100644 index 0000000000..fcbc24306b --- /dev/null +++ b/src/components/ha-code-editor.ts @@ -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 { + const loaded = await loadCodeMirror(); + + const codeMirror = loaded.codeMirror; + + const shadowRoot = this.attachShadow({ mode: "open" }); + + shadowRoot!.innerHTML = ` + `; + + 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; + } +} diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts deleted file mode 100644 index 11190cbfef..0000000000 --- a/src/components/ha-yaml-editor.ts +++ /dev/null @@ -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 = ` - `; - } - - 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; - } -} diff --git a/src/panels/config/js/preact-types.ts b/src/panels/config/js/preact-types.ts index f103f63230..617fe5d2dd 100644 --- a/src/panels/config/js/preact-types.ts +++ b/src/panels/config/js/preact-types.ts @@ -20,7 +20,7 @@ declare global { "ha-device-picker": any; "ha-device-condition-picker": any; "ha-textarea": any; - "ha-yaml-editor": any; + "ha-code-editor": any; "ha-service-picker": any; "mwc-button": any; "ha-device-trigger-picker": any; diff --git a/src/panels/config/js/yaml_textarea.tsx b/src/panels/config/js/yaml_textarea.tsx index f9e8edf634..5050576db2 100644 --- a/src/panels/config/js/yaml_textarea.tsx +++ b/src/panels/config/js/yaml_textarea.tsx @@ -1,8 +1,6 @@ import { h, Component } from "preact"; import yaml from "js-yaml"; -import "../../../components/ha-yaml-editor"; -// tslint:disable-next-line -import { HaYamlEditor } from "../../../components/ha-yaml-editor"; +import "../../../components/ha-code-editor"; const isEmpty = (obj: object) => { for (const key in obj) { @@ -14,8 +12,6 @@ const isEmpty = (obj: object) => { }; export default class YAMLTextArea extends Component { - private _yamlEditor!: HaYamlEditor; - constructor(props) { super(props); @@ -63,12 +59,6 @@ export default class YAMLTextArea extends Component { } } - public componentDidMount() { - setTimeout(() => { - this._yamlEditor.codemirror.refresh(); - }, 1); - } - public render({ label }, { value, isValid }) { const style: any = { minWidth: 300, @@ -77,16 +67,14 @@ export default class YAMLTextArea extends Component { return (

{label}

-
); } - - private _storeYamlEditorRef = (yamlEditor) => (this._yamlEditor = yamlEditor); } diff --git a/src/panels/developer-tools/event/developer-tools-event.js b/src/panels/developer-tools/event/developer-tools-event.js index 25fd569034..c1b50bd068 100644 --- a/src/panels/developer-tools/event/developer-tools-event.js +++ b/src/panels/developer-tools/event/developer-tools-event.js @@ -1,17 +1,18 @@ import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import yaml from "js-yaml"; +import "../../../components/ha-code-editor"; import "../../../resources/ha-style"; import "./events-list"; import "./event-subscribe-card"; import { EventsMixin } from "../../../mixins/events-mixin"; +const ERROR_SENTINEL = {}; /* * @appliesMixin EventsMixin */ @@ -32,6 +33,11 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { .ha-form { margin-right: 16px; + max-width: 400px; + } + + mwc-button { + margin-top: 8px; } .header { @@ -62,11 +68,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { required value="{{eventType}}" > - - Fire Event +

Event Data (YAML, optional)

+ + Fire Event @@ -97,6 +108,16 @@ class HaPanelDevEvent extends EventsMixin(PolymerElement) { type: String, 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; } - fireEvent() { - var eventData; - + _computeParsedEventData(eventData) { try { - eventData = this.eventData ? yaml.safeLoad(this.eventData) : {}; + return eventData.trim() ? yaml.safeLoad(eventData) : {}; } catch (err) { - /* eslint-disable no-alert */ - alert("Error parsing YAML: " + err); - /* eslint-enable no-alert */ + return ERROR_SENTINEL; + } + } + + _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; } - - this.hass.callApi("POST", "events/" + this.eventType, eventData).then( + this.hass.callApi("POST", "events/" + this.eventType, this.parsedJSON).then( function() { this.fire("hass-notification", { message: "Event " + this.eventType + " successful fired!", diff --git a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts index 3320eb52c3..93f0215ee9 100644 --- a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts +++ b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts @@ -9,12 +9,12 @@ import { } from "lit-element"; import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "@polymer/paper-input/paper-textarea"; import { HomeAssistant } from "../../../types"; import { haStyle } from "../../../resources/styles"; import "../../../components/ha-card"; +import "../../../components/ha-code-editor"; import "./mqtt-subscribe-card"; @customElement("developer-tools-mqtt") @@ -48,12 +48,12 @@ class HaPanelDevMqtt extends LitElement { @value-changed=${this._handleTopic} > - Payload (template allowed)

+
+ >
Publish diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js index 1656816d12..de04bd4ed9 100644 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "@polymer/paper-input/paper-textarea"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; @@ -7,6 +6,7 @@ import yaml from "js-yaml"; import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity"; import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-code-editor"; import "../../../components/ha-service-picker"; import "../../../resources/ha-style"; import "../../../util/app-localstorage-document"; @@ -30,6 +30,10 @@ class HaPanelDevService extends PolymerElement { max-width: 400px; } + mwc-button { + margin-top: 8px; + } + .description { margin-top: 24px; white-space: pre-wrap; @@ -109,20 +113,16 @@ class HaPanelDevService extends PolymerElement { allow-custom-entity > - +

Service Data (YAML, optional)

+ Call Service -