diff --git a/package.json b/package.json index 231ad11ac4..91464b3481 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@webcomponents/webcomponentsjs": "^2.2.0", "chart.js": "~2.7.2", "chartjs-chart-timeline": "^0.2.1", + "codemirror": "^5.43.0", "deep-clone-simple": "^1.1.1", "es6-object-assign": "^1.1.0", "eslint-import-resolver-webpack": "^0.10.1", @@ -102,6 +103,7 @@ "@babel/preset-typescript": "^7.1.0", "@gfx/zopfli": "^1.0.9", "@types/chai": "^4.1.7", + "@types/codemirror": "^0.0.71", "@types/mocha": "^5.2.5", "babel-eslint": "^10", "babel-loader": "^8.0.4", diff --git a/src/panels/lovelace/components/hui-yaml-editor.ts b/src/panels/lovelace/components/hui-yaml-editor.ts new file mode 100644 index 0000000000..f83d392bee --- /dev/null +++ b/src/panels/lovelace/components/hui-yaml-editor.ts @@ -0,0 +1,93 @@ +// @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"; +declare global { + interface HASSDomEvents { + "yaml-changed": { + value: string; + }; + } +} + +export class HuiYamlEditor extends HTMLElement { + public codemirror: CodeMirror; + private _value: string; + + public constructor() { + super(); + 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(); + } + + get hasComments(): boolean { + return this.shadowRoot!.querySelector("span.cm-comment") ? true : false; + } + + public connectedCallback(): void { + if (!this.codemirror) { + this.codemirror = CodeMirror(this.shadowRoot, { + value: this._value, + lineNumbers: true, + mode: "yaml", + tabSize: 2, + autofocus: true, + extraKeys: { + Tab: (cm: CodeMirror) => { + const spaces = Array(cm.getOption("indentUnit") + 1).join(" "); + cm.replaceSelection(spaces); + }, + }, + }); + this.codemirror.on("changes", () => this._onChange()); + } else { + this.codemirror.refresh(); + } + } + + private _onChange(): void { + fireEvent(this, "yaml-changed", { value: this.codemirror.getValue() }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-yaml-editor": HuiYamlEditor; + } +} + +window.customElements.define("hui-yaml-editor", HuiYamlEditor); diff --git a/src/panels/lovelace/editor/card-editor/hui-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-edit-card.ts index 7eac2fba87..b11007465f 100644 --- a/src/panels/lovelace/editor/card-editor/hui-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-edit-card.ts @@ -23,23 +23,22 @@ import { HomeAssistant } from "../../../../types"; import { LovelaceCardConfig } from "../../../../data/lovelace"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "./hui-yaml-editor"; +import "../../components/hui-yaml-editor"; +// This is not a duplicate import, one is for types, one is for element. +// tslint:disable-next-line +import { HuiYamlEditor } from "../../components/hui-yaml-editor"; import "./hui-card-preview"; // This is not a duplicate import, one is for types, one is for element. // tslint:disable-next-line import { HuiCardPreview } from "./hui-card-preview"; import { LovelaceCardEditor, Lovelace } from "../../types"; -import { YamlChangedEvent, ConfigValue, ConfigError } from "../types"; -import { extYamlSchema } from "../yaml-ext-schema"; +import { ConfigValue, ConfigError } from "../types"; import { EntityConfig } from "../../entity-rows/types"; import { getCardElementTag } from "../../common/get-card-element-tag"; import { addCard, replaceCard } from "../config-util"; declare global { interface HASSDomEvents { - "yaml-changed": { - yaml: string; - }; "entities-changed": { entities: EntityConfig[]; }; @@ -120,8 +119,7 @@ export class HuiEditCard extends LitElement { ? this._configElement : html` `} @@ -195,6 +193,11 @@ export class HuiEditCard extends LitElement { await this.updateComplete; this._loading = false; this._resizeDialog(); + if (!this._uiEditor) { + setTimeout(() => { + this.yamlEditor.codemirror.refresh(); + }, 1); + } } private async _resizeDialog(): Promise { @@ -217,9 +220,7 @@ export class HuiEditCard extends LitElement { const cardConf: LovelaceCardConfig = this._configValue!.format === "yaml" - ? yaml.safeLoad(this._configValue!.value!, { - schema: extYamlSchema, - }) + ? yaml.safeLoad(this._configValue!.value!) : this._configValue!.value!; try { @@ -241,12 +242,12 @@ export class HuiEditCard extends LitElement { } } - private _handleYamlChanged(ev: YamlChangedEvent): void { - this._configValue = { format: "yaml", value: ev.detail.yaml }; + private _handleYamlChanged(ev: CustomEvent): void { + this._configValue = { format: "yaml", value: ev.detail.value }; try { - const config = yaml.safeLoad(this._configValue.value, { - schema: extYamlSchema, - }) as LovelaceCardConfig; + const config = yaml.safeLoad( + this._configValue.value + ) as LovelaceCardConfig; this._updatePreview(config); this._configState = "OK"; } catch (err) { @@ -263,7 +264,9 @@ export class HuiEditCard extends LitElement { this._updatePreview(value); } - private _updatePreview(config: LovelaceCardConfig) { + private async _updatePreview(config: LovelaceCardConfig) { + await this.updateComplete; + if (!this._previewEl) { return; } @@ -295,9 +298,7 @@ export class HuiEditCard extends LitElement { this._uiEditor = !this._uiEditor; } else if (this._configElement && this._configValue!.format === "yaml") { const yamlConfig = this._configValue!.value; - const cardConfig = yaml.safeLoad(yamlConfig, { - schema: extYamlSchema, - }) as LovelaceCardConfig; + const cardConfig = yaml.safeLoad(yamlConfig) as LovelaceCardConfig; this._uiEditor = !this._uiEditor; if (cardConfig.type !== this._cardType) { const succes = await this._loadConfigElement(cardConfig); @@ -356,6 +357,7 @@ export class HuiEditCard extends LitElement { configElement = await elClass.getConfigElement(); } else { this._configValue = { format: "yaml", value: yaml.safeDump(conf) }; + this._updatePreview(conf); this._uiEditor = false; this._configElement = null; return false; @@ -372,6 +374,7 @@ export class HuiEditCard extends LitElement { format: "yaml", value: yaml.safeDump(conf), }; + this._updatePreview(conf); this._uiEditor = false; this._configElement = null; return false; @@ -398,6 +401,10 @@ export class HuiEditCard extends LitElement { } } + private get yamlEditor(): HuiYamlEditor { + return this.shadowRoot!.querySelector("hui-yaml-editor")!; + } + static get styles(): CSSResult[] { return [ haStyleDialog, diff --git a/src/panels/lovelace/editor/card-editor/hui-yaml-editor.ts b/src/panels/lovelace/editor/card-editor/hui-yaml-editor.ts deleted file mode 100644 index df2767d902..0000000000 --- a/src/panels/lovelace/editor/card-editor/hui-yaml-editor.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - html, - LitElement, - PropertyDeclarations, - TemplateResult, -} from "lit-element"; -import "@polymer/paper-input/paper-textarea"; - -import { HomeAssistant } from "../../../../types"; -import { fireEvent } from "../../../../common/dom/fire_event"; - -export class HuiYAMLEditor extends LitElement { - protected hass?: HomeAssistant; - private _yaml?: string; - - static get properties(): PropertyDeclarations { - return { _yaml: {} }; - } - - set yaml(yaml: string) { - if (yaml === undefined) { - return; - } else { - this._yaml = yaml; - } - } - - protected render(): TemplateResult | void { - return html` - ${this.renderStyle()} - - `; - } - - private renderStyle(): TemplateResult { - return html` - - `; - } - - private _valueChanged(ev: Event): void { - const target = ev.target! as any; - this._yaml = target.value; - fireEvent(this, "yaml-changed", { - yaml: target.value, - }); - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-yaml-editor": HuiYAMLEditor; - } -} - -customElements.define("hui-yaml-editor", HuiYAMLEditor); diff --git a/src/panels/lovelace/editor/yaml-ext-schema.ts b/src/panels/lovelace/editor/yaml-ext-schema.ts deleted file mode 100644 index 357962d911..0000000000 --- a/src/panels/lovelace/editor/yaml-ext-schema.ts +++ /dev/null @@ -1,22 +0,0 @@ -import yaml from "js-yaml"; - -const secretYamlType = new yaml.Type("!secret", { - kind: "scalar", - construct(data) { - data = data || ""; - return "!secret " + data; - }, -}); - -const includeYamlType = new yaml.Type("!include", { - kind: "scalar", - construct(data) { - data = data || ""; - return "!include " + data; - }, -}); - -export const extYamlSchema = yaml.Schema.create([ - secretYamlType, - includeYamlType, -]); diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index 9e44f7cc32..8d1c201fb9 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -14,8 +14,10 @@ import { Lovelace } from "./types"; import "../../components/ha-icon"; import { haStyle } from "../../resources/ha-style"; - -const TAB_INSERT = " "; +import "./components/hui-yaml-editor"; +// This is not a duplicate import, one is for types, one is for element. +// tslint:disable-next-line +import { HuiYamlEditor } from "./components/hui-yaml-editor"; const lovelaceStruct = struct.interface({ title: "string?", @@ -28,16 +30,13 @@ class LovelaceFullConfigEditor extends LitElement { public closeEditor?: () => void; private _saving?: boolean; private _changed?: boolean; - private _hashAdded?: boolean; - private _hash?: boolean; + private _generation?: number; static get properties() { return { lovelace: {}, _saving: {}, _changed: {}, - _hashAdded: {}, - _hash: {}, }; } @@ -51,11 +50,6 @@ class LovelaceFullConfigEditor extends LitElement { @click="${this._closeEditor}" >
Edit Config
- ${this._hash - ? html` - Comments will be not be saved! - ` - : ""} Save - + + `; } protected firstUpdated() { - const textArea = this.textArea; - textArea.value = yaml.safeDump(this.lovelace!.config); - textArea.addEventListener("keydown", (e) => { - if (e.keyCode === 51) { - this._hashAdded = true; - return; - } - - if (e.keyCode !== 9) { - return; - } - - e.preventDefault(); - - // tab was pressed, get caret position/selection - const val = textArea.value; - const start = textArea.selectionStart; - const end = textArea.selectionEnd; - - // set textarea value to: text before caret + tab + text after caret - textArea.value = - val.substring(0, start) + TAB_INSERT + val.substring(end); - - // put caret at right position again - textArea.selectionStart = textArea.selectionEnd = - start + TAB_INSERT.length; - }); + this.yamlEditor.value = yaml.safeDump(this.lovelace!.config); + this.yamlEditor.codemirror.clearHistory(); + this._generation = this.yamlEditor.codemirror.changeGeneration(true); } static get styles(): CSSResult[] { return [ haStyle, css` + :host { + --code-mirror-height: 100%; + } + app-header-layout { height: 100vh; } @@ -132,16 +101,8 @@ class LovelaceFullConfigEditor extends LitElement { height: calc(100vh - 68px); } - textarea { - box-sizing: border-box; + hui-code-editor { height: 100%; - width: 100%; - resize: none; - border: 0; - outline: 0; - font-size: 12pt; - font-family: "Courier New", Courier, monospace; - padding: 8px; } .save-button { @@ -158,6 +119,20 @@ class LovelaceFullConfigEditor extends LitElement { ]; } + private _yamlChanged() { + if (!this._generation) { + return; + } + this._changed = !this.yamlEditor.codemirror.isClean(this._generation); + if (this._changed && !window.onbeforeunload) { + window.onbeforeunload = () => { + return true; + }; + } else if (!this._changed && window.onbeforeunload) { + window.onbeforeunload = null; + } + } + private _closeEditor() { if (this._changed) { if ( @@ -173,10 +148,10 @@ class LovelaceFullConfigEditor extends LitElement { private async _handleSave() { this._saving = true; - if (this._hashAdded) { + if (this.yamlEditor.hasComments) { if ( !confirm( - "Your config might contain comments, these will not be saved. Do you want to continue?" + "Your config contains comment(s), these will not be saved. Do you want to continue?" ) ) { return; @@ -185,7 +160,7 @@ class LovelaceFullConfigEditor extends LitElement { let value; try { - value = yaml.safeLoad(this.textArea.value); + value = yaml.safeLoad(this.yamlEditor.value); } catch (err) { alert(`Unable to parse YAML: ${err}`); this._saving = false; @@ -202,25 +177,14 @@ class LovelaceFullConfigEditor extends LitElement { } catch (err) { alert(`Unable to save YAML: ${err}`); } + this._generation = this.yamlEditor.codemirror.changeGeneration(true); window.onbeforeunload = null; this._saving = false; this._changed = false; - this._hashAdded = false; } - private _yamlChanged() { - this._hash = this._hashAdded || this.textArea.value.includes("#"); - if (this._changed) { - return; - } - window.onbeforeunload = () => { - return true; - }; - this._changed = true; - } - - private get textArea(): HTMLTextAreaElement { - return this.shadowRoot!.querySelector("textarea")!; + private get yamlEditor(): HuiYamlEditor { + return this.shadowRoot!.querySelector("hui-yaml-editor")!; } } diff --git a/yarn.lock b/yarn.lock index e7c12feae1..c054a8417e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1436,6 +1436,13 @@ resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614" integrity sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ= +"@types/codemirror@^0.0.71": + version "0.0.71" + resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.71.tgz#861f1bcb3100c0a064567c5400f2981cf4ae8ca7" + integrity sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w== + dependencies: + "@types/tern" "*" + "@types/compression@^0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.33.tgz#95dc733a2339aa846381d7f1377792d2553dc27d" @@ -1477,7 +1484,7 @@ resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a" integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw== -"@types/estree@0.0.39": +"@types/estree@*", "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== @@ -1850,6 +1857,13 @@ dependencies: "@types/node" "*" +"@types/tern@*": + version "0.22.1" + resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.22.1.tgz#d96467553128794f42fbe7ba8f60b520acffb817" + integrity sha512-CRzPRkg8hYLwunsj61r+rqPJQbiCIEQqlMMY/0k7krgIsoSaFgGg1ZH2f9qaR1YpenaMl6PnlTtUkCbNH/uo+A== + dependencies: + "@types/estree" "*" + "@types/through@*": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.29.tgz#72943aac922e179339c651fa34a4428a4d722f93" @@ -4281,6 +4295,11 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +codemirror@^5.43.0: + version "5.43.0" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.43.0.tgz#2454b5e0f7005dc9945ab7b0d9594ccf233da040" + integrity sha512-mljwQWUaWIf85I7QwTBryF2ASaIvmYAL4s5UCanCJFfKeXOKhrqdHWdHiZWAMNT+hjLTCnVx2S/SYTORIgxsgA== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"