Add crosshairs, destroy globals, and tweak updates for code editor (#17302)

* Add crosshairs, destroy globals, and tweak updates for code editor

* Define update listener as arrow function

* Ensure editor is recreated on reconnection

* Don't create code mirror multiple times

* Remove creation in update

* Leverage lit lifecycle for editor creation and destruction

* Bump @codemirror packages

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Steve Repsher 2023-08-04 09:16:37 -04:00 committed by GitHub
parent 0d630aa5f5
commit 716e68fc5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 51 additions and 41 deletions

View File

@ -4,7 +4,7 @@ import type {
CompletionResult, CompletionResult,
CompletionSource, CompletionSource,
} from "@codemirror/autocomplete"; } from "@codemirror/autocomplete";
import type { Extension } from "@codemirror/state"; import type { Extension, TransactionSpec } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
@ -12,7 +12,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation"; import { stopPropagation } from "../common/dom/stop_propagation";
import { loadCodeMirror } from "../resources/codemirror.ondemand"; import { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon"; import "./ha-icon";
@ -54,11 +54,11 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "autocomplete-icons" }) @property({ type: Boolean, attribute: "autocomplete-icons" })
public autocompleteIcons = false; public autocompleteIcons = false;
@property() public error = false; @property({ type: Boolean }) public error = false;
@state() private _value = ""; @state() private _value = "";
private _loadedCodeMirror?: typeof import("../resources/codemirror"); private _loadedCodeMirror?: CodeMirror;
private _iconList?: Completion[]; private _iconList?: Completion[];
@ -78,12 +78,19 @@ export class HaCodeEditor extends ReactiveElement {
this.codemirror.state, this.codemirror.state,
[this._loadedCodeMirror.tags.comment] [this._loadedCodeMirror.tags.comment]
); );
return !!this.shadowRoot!.querySelector(`span.${className}`); return !!this.renderRoot.querySelector(`span.${className}`);
} }
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// Force update on reconnection so editor is recreated
if (this.hasUpdated) {
this.requestUpdate();
}
this.addEventListener("keydown", stopPropagation); this.addEventListener("keydown", stopPropagation);
// This is unreachable as editor will not exist yet,
// but focus should not behave like this for good a11y.
// (@steverep to fix in autofocus PR)
if (!this.codemirror) { if (!this.codemirror) {
return; return;
} }
@ -95,31 +102,41 @@ export class HaCodeEditor extends ReactiveElement {
public disconnectedCallback() { public disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation); this.removeEventListener("keydown", stopPropagation);
this.updateComplete.then(() => {
this.codemirror!.destroy();
delete this.codemirror;
});
}
// Ensure CodeMirror module is loaded before any update
protected override async scheduleUpdate() {
this._loadedCodeMirror ??= await loadCodeMirror();
super.scheduleUpdate();
} }
protected update(changedProps: PropertyValues): void { protected update(changedProps: PropertyValues): void {
super.update(changedProps); super.update(changedProps);
if (!this.codemirror) { if (!this.codemirror) {
this._createCodeMirror();
return; return;
} }
const transactions: TransactionSpec[] = [];
if (changedProps.has("mode")) { if (changedProps.has("mode")) {
this.codemirror.dispatch({ transactions.push({
effects: this._loadedCodeMirror!.langCompartment!.reconfigure( effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode this._mode
), ),
}); });
} }
if (changedProps.has("readOnly")) { if (changedProps.has("readOnly")) {
this.codemirror.dispatch({ transactions.push({
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure( effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly) this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
), ),
}); });
} }
if (changedProps.has("_value") && this._value !== this.value) { if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.dispatch({ transactions.push({
changes: { changes: {
from: 0, from: 0,
to: this.codemirror.state.doc.length, to: this.codemirror.state.doc.length,
@ -127,46 +144,45 @@ export class HaCodeEditor extends ReactiveElement {
}, },
}); });
} }
if (transactions.length > 0) {
this.codemirror.dispatch(...transactions);
}
if (changedProps.has("error")) { if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error); this.classList.toggle("error-state", this.error);
} }
} }
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._load();
}
private get _mode() { private get _mode() {
return this._loadedCodeMirror!.langs[this.mode]; return this._loadedCodeMirror!.langs[this.mode];
} }
private async _load(): Promise<void> { private _createCodeMirror() {
this._loadedCodeMirror = await loadCodeMirror(); if (!this._loadedCodeMirror) {
throw new Error("Cannot create editor before CodeMirror is loaded");
}
const extensions: Extension[] = [ const extensions: Extension[] = [
this._loadedCodeMirror.lineNumbers(), this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(), this._loadedCodeMirror.history(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.crosshairCursor(),
this._loadedCodeMirror.highlightSelectionMatches(), this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(), this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([ this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap, ...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap, ...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap, ...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings, ...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding, saveKeyBinding,
] as KeyBinding[]), ]),
this._loadedCodeMirror.langCompartment.of(this._mode), this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.haTheme, this._loadedCodeMirror.haTheme,
this._loadedCodeMirror.haSyntaxHighlighting, this._loadedCodeMirror.haSyntaxHighlighting,
this._loadedCodeMirror.readonlyCompartment.of( this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
), ),
this._loadedCodeMirror.EditorView.updateListener.of((update) => this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
this._onUpdate(update)
),
]; ];
if (!this.readOnly) { if (!this.readOnly) {
@ -192,8 +208,7 @@ export class HaCodeEditor extends ReactiveElement {
doc: this._value, doc: this._value,
extensions, extensions,
}), }),
root: this.shadowRoot!, parent: this.renderRoot,
parent: this.shadowRoot!,
}); });
} }
@ -277,17 +292,13 @@ export class HaCodeEditor extends ReactiveElement {
}; };
} }
private _onUpdate(update: ViewUpdate): void { private _onUpdate = (update: ViewUpdate): void => {
if (!update.docChanged) { if (!update.docChanged) {
return; return;
} }
const newValue = this.value; this._value = update.state.doc.toString();
if (newValue === this._value) {
return;
}
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value }); fireEvent(this, "value-changed", { value: this._value });
} };
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`

View File

@ -27,7 +27,7 @@ export class HaYamlEditor extends LitElement {
@property() public defaultValue?: any; @property() public defaultValue?: any;
@property() public isValid = true; @property({ type: Boolean }) public isValid = true;
@property() public label?: string; @property() public label?: string;

View File

@ -1,10 +1,8 @@
let loaded: Promise<typeof import("./codemirror")>; export type CodeMirror = typeof import("./codemirror");
export const loadCodeMirror = async (): Promise< let loaded: CodeMirror;
typeof import("./codemirror")
> => { export const loadCodeMirror = async () => {
if (!loaded) { loaded ??= await import("./codemirror");
loaded = import("./codemirror");
}
return loaded; return loaded;
}; };

View File

@ -16,6 +16,7 @@ export { highlightingFor } from "@codemirror/language";
export { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; export { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
export { EditorState } from "@codemirror/state"; export { EditorState } from "@codemirror/state";
export { export {
crosshairCursor,
drawSelection, drawSelection,
EditorView, EditorView,
highlightActiveLine, highlightActiveLine,