Files
plainleaf/client/codemirror/top_bottom_panels.ts
T
2026-03-12 13:40:20 +01:00

147 lines
4.1 KiB
TypeScript

import type { EditorState } from "@codemirror/state";
import { Decoration, WidgetType } from "@codemirror/view";
import type { Client } from "../client.ts";
import { decoratorStateField } from "./util.ts";
import { LuaWidget, type LuaWidgetContent } from "./lua_widget.ts";
import { activeWidgets } from "./code_widget.ts";
class ArrayWidget extends WidgetType {
public dom?: HTMLElement;
constructor(
readonly client: Client,
readonly cacheKey: string,
readonly callback: (pageName: string) => Promise<LuaWidgetContent[] | null>,
readonly childClass: string,
) {
super();
}
override get estimatedHeight(): number {
return this.client.widgetCache.getCachedWidgetHeight(this.cacheKey);
}
toDOM(): HTMLElement {
activeWidgets.add(this);
const div = document.createElement("div");
div.className = "sb-widget-array";
// This doesn't do that much, but it also doesn't really hurt
const cacheItem = this.client.widgetCache.getWidgetCache(this.cacheKey);
if (cacheItem) {
div.innerHTML = cacheItem.html;
}
// Async kick-off of content renderer
this.renderContent(div).catch(console.error);
this.dom = div;
return div;
}
async renderContent(div: HTMLElement) {
const content = await this.callback(this.client.currentName());
if (!content) return;
const renderedWidgets: HTMLElement[] = [];
for (const [i, widgetContent] of content.entries()) {
// Filter out any "empty" widgets. Leaving the content empty, but
// returning a valid widgets, seems to be a common pattern
if (
!widgetContent ||
widgetContent === "" ||
(widgetContent instanceof Object &&
!widgetContent.markdown &&
!widgetContent.html)
)
continue;
const widget = new LuaWidget({
client: this.client,
cacheKey: `${this.cacheKey}:${i}`,
expressionText: "",
callback: () => Promise.resolve(widgetContent),
inPage: false,
});
// Throw away the wrapper, as it only causes trouble and we are rewrapping
// anyways
const html = widget.toDOM().querySelector<HTMLDivElement>(":scope > div");
if (!html) {
// This should never really happen, just in case
console.log("There was an error rendering one of the panel widgets");
continue;
}
html.classList.add(this.childClass);
renderedWidgets.push(html);
}
if (renderedWidgets.length === 0) {
div.style.display = "none";
return;
}
div.replaceChildren(...renderedWidgets);
// Wait for the clientHeight to settle
setTimeout(() => {
this.client.widgetCache.setWidgetCache(this.cacheKey, {
block: true,
html: div.innerHTML,
});
this.client.widgetCache.setCachedWidgetHeight(
this.cacheKey,
div.clientHeight,
);
});
}
override eq(other: WidgetType): boolean {
// This class isn't really used for stuff that's updated. If that's
// needed in the future, one could e.g. add a `bodyText` property again
return other instanceof ArrayWidget && other.cacheKey === this.cacheKey;
}
}
export function postScriptPrefacePlugin(editor: Client) {
return decoratorStateField((state: EditorState) => {
if (!editor.clientSystem.scriptsLoaded) {
// console.info("System not yet ready, not rendering panel widgets.");
return Decoration.none;
}
const widgets: any[] = [];
widgets.push(
Decoration.widget({
widget: new ArrayWidget(
editor,
`top:lua:${editor.currentPath()}`,
async () => await client.dispatchAppEvent("hooks:renderTopWidgets"),
"sb-lua-top-widget",
),
side: -1,
block: true,
}).range(0),
);
widgets.push(
Decoration.widget({
widget: new ArrayWidget(
editor,
`bottom:lua:${editor.currentPath()}`,
async () =>
await client.dispatchAppEvent("hooks:renderBottomWidgets"),
"sb-lua-bottom-widget",
),
side: 1,
block: true,
}).range(state.doc.length),
);
return Decoration.set(widgets);
});
}