mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Fix code editor autocomplete using wa popup (#30081)
This commit is contained in:
committed by
Bram Kragten
parent
cfa8eb5370
commit
ee77619da3
@@ -1,3 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
@@ -110,6 +112,18 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
private _completionInfoPopover?: WaPopup;
|
||||
|
||||
private _completionInfoContainer?: HTMLDivElement;
|
||||
|
||||
private _completionInfoDestroy?: () => void;
|
||||
|
||||
private _completionInfoRequest = 0;
|
||||
|
||||
private _completionInfoKey?: string;
|
||||
|
||||
private _completionInfoFrame?: number;
|
||||
|
||||
private _editorToolbar?: HaIconButtonToolbar;
|
||||
|
||||
private _iconList?: Completion[];
|
||||
@@ -155,6 +169,14 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
public disconnectedCallback() {
|
||||
fireEvent(this, "dialog-set-fullscreen", false);
|
||||
this._clearCompletionInfo();
|
||||
if (this._completionInfoFrame !== undefined) {
|
||||
cancelAnimationFrame(this._completionInfoFrame);
|
||||
this._completionInfoFrame = undefined;
|
||||
}
|
||||
this._completionInfoPopover?.remove();
|
||||
this._completionInfoPopover = undefined;
|
||||
this._completionInfoContainer = undefined;
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", stopPropagation);
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
@@ -288,6 +310,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.foldingCompartment.of(
|
||||
this._getFoldingExtensions()
|
||||
),
|
||||
this._loadedCodeMirror.tooltips({
|
||||
position: "absolute",
|
||||
}),
|
||||
...(this.placeholder ? [placeholder(this.placeholder)] : []),
|
||||
];
|
||||
|
||||
@@ -595,6 +620,157 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return completionInfo;
|
||||
};
|
||||
|
||||
private _getCompletionInfo = (
|
||||
completion: Completion
|
||||
): CompletionInfo | Promise<CompletionInfo> | null => {
|
||||
if (this.hass && completion.label in this.hass.states) {
|
||||
return this._renderInfo(completion);
|
||||
}
|
||||
|
||||
if (completion.label.startsWith("mdi:")) {
|
||||
return renderIcon(completion);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private _ensureCompletionInfoPopover(): WaPopup {
|
||||
if (!this._completionInfoPopover) {
|
||||
this._completionInfoPopover = document.createElement(
|
||||
"wa-popup"
|
||||
) as WaPopup;
|
||||
this._completionInfoPopover.classList.add("completion-info-popover");
|
||||
this._completionInfoPopover.placement = "right-start";
|
||||
this._completionInfoPopover.distance = 4;
|
||||
this._completionInfoPopover.flip = true;
|
||||
this._completionInfoPopover.flipFallbackPlacements =
|
||||
"left-start bottom-start top-start";
|
||||
this._completionInfoPopover.shift = true;
|
||||
this._completionInfoPopover.shiftPadding = 8;
|
||||
this._completionInfoPopover.autoSize = "both";
|
||||
this._completionInfoPopover.autoSizePadding = 8;
|
||||
|
||||
this._completionInfoContainer = document.createElement("div");
|
||||
this._completionInfoPopover.appendChild(this._completionInfoContainer);
|
||||
this.renderRoot.appendChild(this._completionInfoPopover);
|
||||
}
|
||||
|
||||
return this._completionInfoPopover;
|
||||
}
|
||||
|
||||
private _clearCompletionInfo() {
|
||||
this._completionInfoRequest += 1;
|
||||
this._completionInfoKey = undefined;
|
||||
this._completionInfoDestroy?.();
|
||||
this._completionInfoDestroy = undefined;
|
||||
this._completionInfoContainer?.replaceChildren();
|
||||
|
||||
if (this._completionInfoPopover?.active) {
|
||||
this._completionInfoPopover.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderCompletionInfoContent(info: CompletionInfo) {
|
||||
this._completionInfoDestroy?.();
|
||||
this._completionInfoDestroy = undefined;
|
||||
|
||||
if (!this._completionInfoContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info === null) {
|
||||
this._completionInfoContainer.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
if ("nodeType" in info) {
|
||||
this._completionInfoContainer.replaceChildren(info);
|
||||
return;
|
||||
}
|
||||
|
||||
this._completionInfoContainer.replaceChildren(info.dom);
|
||||
this._completionInfoDestroy = info.destroy;
|
||||
}
|
||||
|
||||
private _syncCompletionInfoPopover = () => {
|
||||
if (this._completionInfoFrame !== undefined) {
|
||||
cancelAnimationFrame(this._completionInfoFrame);
|
||||
}
|
||||
|
||||
this._completionInfoFrame = requestAnimationFrame(() => {
|
||||
this._completionInfoFrame = undefined;
|
||||
this._syncCompletionInfoPopoverNow();
|
||||
});
|
||||
};
|
||||
|
||||
private _syncCompletionInfoPopoverNow = () => {
|
||||
if (!this.codemirror || !this._loadedCodeMirror) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.matchMedia("(max-width: 600px)").matches) {
|
||||
this._clearCompletionInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const completion = this._loadedCodeMirror.selectedCompletion(
|
||||
this.codemirror.state
|
||||
);
|
||||
const selectedOption = this.codemirror.dom.querySelector(
|
||||
".cm-tooltip-autocomplete li[aria-selected]"
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (!completion || !selectedOption) {
|
||||
this._clearCompletionInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const infoResult = this._getCompletionInfo(completion);
|
||||
|
||||
if (!infoResult) {
|
||||
this._clearCompletionInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = ++this._completionInfoRequest;
|
||||
const infoKey = completion.label;
|
||||
const popover = this._ensureCompletionInfoPopover();
|
||||
popover.anchor = selectedOption;
|
||||
|
||||
const showPopover = async (info: CompletionInfo) => {
|
||||
if (requestId !== this._completionInfoRequest) {
|
||||
if (info && typeof info === "object" && "destroy" in info) {
|
||||
info.destroy?.();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (infoKey !== this._completionInfoKey) {
|
||||
this._renderCompletionInfoContent(info);
|
||||
this._completionInfoKey = infoKey;
|
||||
}
|
||||
|
||||
await popover.updateComplete;
|
||||
popover.active = true;
|
||||
popover.reposition();
|
||||
};
|
||||
|
||||
if ("then" in infoResult) {
|
||||
infoResult.then(showPopover).catch(() => {
|
||||
if (requestId === this._completionInfoRequest) {
|
||||
this._clearCompletionInfo();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showPopover(infoResult).catch(() => {
|
||||
if (requestId === this._completionInfoRequest) {
|
||||
this._clearCompletionInfo();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
||||
if (!states) {
|
||||
return [];
|
||||
@@ -604,7 +780,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
type: "variable",
|
||||
label: key,
|
||||
detail: states[key].attributes.friendly_name,
|
||||
info: this._renderInfo,
|
||||
}));
|
||||
|
||||
return options;
|
||||
@@ -778,7 +953,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
type: "variable",
|
||||
label: `mdi:${icon.name}`,
|
||||
detail: icon.keywords.join(", "),
|
||||
info: renderIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -806,6 +980,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _onUpdate = (update: ViewUpdate): void => {
|
||||
this._canUndo = !this.readOnly && undoDepth(update.state) > 0;
|
||||
this._canRedo = !this.readOnly && redoDepth(update.state) > 0;
|
||||
this._syncCompletionInfoPopover();
|
||||
if (!update.docChanged) {
|
||||
return;
|
||||
}
|
||||
@@ -925,9 +1100,31 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
wa-popup.completion-info-popover {
|
||||
--auto-size-available-width: min(
|
||||
420px,
|
||||
calc(var(--safe-width) - var(--ha-space-8))
|
||||
);
|
||||
}
|
||||
|
||||
wa-popup.completion-info-popover::part(popup) {
|
||||
padding: 0;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(
|
||||
--code-editor-background-color,
|
||||
var(--card-background-color)
|
||||
);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--mdc-shape-medium, 4px);
|
||||
box-shadow:
|
||||
0px 5px 5px -3px rgb(0 0 0 / 20%),
|
||||
0px 8px 10px 1px rgb(0 0 0 / 14%),
|
||||
0px 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
wa-popup.completion-info-popover,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { KeyBinding } from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
export { autocompletion } from "@codemirror/autocomplete";
|
||||
export { autocompletion, selectedCompletion } from "@codemirror/autocomplete";
|
||||
export { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
export { highlightingFor, foldGutter } from "@codemirror/language";
|
||||
export {
|
||||
@@ -32,6 +32,7 @@ export {
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
dropCursor,
|
||||
tooltips,
|
||||
} from "@codemirror/view";
|
||||
export { indentationMarkers } from "@replit/codemirror-indentation-markers";
|
||||
export { tags } from "@lezer/highlight";
|
||||
@@ -151,10 +152,22 @@ export const haTheme = EditorView.theme({
|
||||
"var(--code-editor-background-color, var(--card-background-color))",
|
||||
border: "1px solid var(--divider-color)",
|
||||
borderRadius: "var(--mdc-shape-medium, 4px)",
|
||||
maxWidth: "min(420px, calc(var(--safe-width) - var(--ha-space-8)))",
|
||||
boxSizing: "border-box",
|
||||
boxShadow:
|
||||
"0px 5px 5px -3px rgb(0 0 0 / 20%), 0px 8px 10px 1px rgb(0 0 0 / 14%), 0px 3px 14px 2px rgb(0 0 0 / 12%)",
|
||||
},
|
||||
|
||||
".cm-tooltip.cm-tooltip-autocomplete": {
|
||||
maxWidth:
|
||||
"min(420px, calc(var(--safe-width) - var(--ha-space-8)), calc(100% - var(--ha-space-2)))",
|
||||
},
|
||||
|
||||
".cm-tooltip-autocomplete > ul": {
|
||||
maxWidth: "100%",
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
|
||||
"& .cm-tooltip.cm-tooltip-autocomplete > ul > li": {
|
||||
padding: "4px 8px",
|
||||
},
|
||||
@@ -177,15 +190,6 @@ export const haTheme = EditorView.theme({
|
||||
color: "var(--text-primary-color)",
|
||||
},
|
||||
|
||||
"& .cm-completionInfo.cm-completionInfo-right": {
|
||||
left: "calc(100% + 4px)",
|
||||
},
|
||||
|
||||
"& .cm-tooltip.cm-completionInfo": {
|
||||
padding: "4px 8px",
|
||||
marginTop: "-5px",
|
||||
},
|
||||
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: "rgba(var(--rgb-primary-color), 0.1)",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user