mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
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:
parent
0d630aa5f5
commit
716e68fc5e
@ -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`
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user