Files
plainleaf/client/codemirror/table.ts
T
Zef Hemel fbaad6cffb Fix widget glitching
No longer cache widget HTML content just widget heights, either way the
widgets will briefly flash as the widget is recalculated and this saves
storage space and reduces issues in Webkit
2026-04-07 15:42:59 +02:00

144 lines
4.2 KiB
TypeScript

import type { EditorState } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { Decoration, WidgetType } from "@codemirror/view";
import {
decoratorStateField,
invisibleDecoration,
isCursorInRange,
} from "./util.ts";
import { renderMarkdownToHtml } from "../markdown_renderer/markdown_render.ts";
import {
type ParseTree,
renderToText,
} from "@silverbulletmd/silverbullet/lib/tree";
import { lezerToParseTree } from "../markdown_parser/parse_tree.ts";
import type { Client } from "../client.ts";
import { expandMarkdown } from "../markdown_renderer/inline.ts";
import {
attachWidgetEventHandlers,
buildTranslateUrls,
} from "./widget_util.ts";
class TableViewWidget extends WidgetType {
tableBodyText: string;
constructor(
readonly client: Client,
readonly t: ParseTree,
) {
super();
this.tableBodyText = renderToText(t);
}
override get estimatedHeight(): number {
return this.client.widgetCache.getCachedWidgetHeight(
`table:${this.tableBodyText}`,
);
}
toDOM(): HTMLElement {
const dom = document.createElement("span");
dom.classList.add("sb-table-widget");
dom.addEventListener("click", (e) => {
// Pulling data-pos to put the cursor in the right place, falling back
// to the start of the table.
const dataAttributes = (e.target as any).dataset;
const fallbackPos = this.client.editorView.posAtDOM(dom, 0);
this.client.editorView.dispatch({
selection: {
anchor: dataAttributes.pos ? +dataAttributes.pos : fallbackPos,
},
});
});
void expandMarkdown(
this.client.space,
this.client.currentName(),
this.t,
this.client.clientSystem.spaceLuaEnv,
{
syntaxExtensions: this.client.config.get("syntaxExtensions", {}),
},
).then((t) => {
dom.innerHTML = renderMarkdownToHtml(t, {
// Annotate every element with its position so we can use it to put
// the cursor there when the user clicks on the table.
annotationPositions: true,
shortWikiLinks: this.client.config.get("shortWikiLinks", false),
translateUrls: buildTranslateUrls(this.client),
});
setTimeout(() => {
// Give it a tick to render
attachWidgetEventHandlers(dom, this.client, this.tableBodyText);
this.client.widgetCache.setCachedWidgetMeta(
`table:${this.tableBodyText}`,
{ height: dom.clientHeight, block: true },
);
});
});
return dom;
}
override eq(other: WidgetType): boolean {
return (
other instanceof TableViewWidget &&
other.tableBodyText === this.tableBodyText
);
}
}
export function tablePlugin(editor: Client) {
return decoratorStateField((state: EditorState) => {
const widgets: any[] = [];
syntaxTree(state).iterate({
enter: (node) => {
const { from, to, name } = node;
if (name !== "Table") return;
if (isCursorInRange(state, [from, to])) return;
const tableText = state.sliceDoc(from, to);
const lineStrings = tableText.split("\n");
const lines: { from: number; to: number }[] = [];
let fromIt = from;
for (const line of lineStrings) {
lines.push({
from: fromIt,
to: fromIt + line.length,
});
fromIt += line.length + 1;
}
const firstLine = lines[0],
lastLine = lines[lines.length - 1];
// In case of doubt, back out
if (!firstLine || !lastLine) return;
widgets.push(invisibleDecoration.range(firstLine.from, firstLine.to));
widgets.push(invisibleDecoration.range(lastLine.from, lastLine.to));
lines.slice(1, lines.length - 1).forEach((line) => {
widgets.push(
Decoration.line({ class: "sb-line-table-outside" }).range(
line.from,
),
);
});
const text = state.sliceDoc(0, to);
widgets.push(
Decoration.widget({
widget: new TableViewWidget(
editor,
lezerToParseTree(text, node.node),
),
}).range(from),
);
},
});
return Decoration.set(widgets, true);
});
}