Top-bottom panel refactor, more instant rendering
This commit is contained in:
@@ -41,6 +41,7 @@ import { clientCodeWidgetSyscalls } from "./syscalls/client_code_widget.ts";
|
||||
import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts";
|
||||
import { deepObjectMerge } from "$sb/lib/json.ts";
|
||||
import { Query } from "$sb/types.ts";
|
||||
import { PanelWidgetHook } from "./hooks/panel_widget.ts";
|
||||
|
||||
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
||||
|
||||
@@ -51,6 +52,7 @@ export class ClientSystem {
|
||||
codeWidgetHook: CodeWidgetHook;
|
||||
mdExtensions: MDExt[] = [];
|
||||
system: System<SilverBulletHooks>;
|
||||
panelWidgetHook: PanelWidgetHook;
|
||||
|
||||
constructor(
|
||||
private client: Client,
|
||||
@@ -83,6 +85,10 @@ export class ClientSystem {
|
||||
this.codeWidgetHook = new CodeWidgetHook();
|
||||
this.system.addHook(this.codeWidgetHook);
|
||||
|
||||
// Panel widget hook
|
||||
this.panelWidgetHook = new PanelWidgetHook();
|
||||
this.system.addHook(this.panelWidgetHook);
|
||||
|
||||
// MQ hook
|
||||
if (client.syncMode) {
|
||||
// Process MQ messages locally
|
||||
|
||||
@@ -70,10 +70,10 @@ export function fencedCodePlugin(editor: Client) {
|
||||
const widget = renderMode === "markdown"
|
||||
? new MarkdownWidget(
|
||||
from + lineStrings[0].length + 1,
|
||||
to - lineStrings[lineStrings.length - 1].length - 1,
|
||||
editor,
|
||||
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
||||
codeWidgetCallback,
|
||||
"sb-markdown-widget",
|
||||
)
|
||||
: new IFrameWidget(
|
||||
from + lineStrings[0].length + 1,
|
||||
|
||||
@@ -11,21 +11,25 @@ export class MarkdownWidget extends WidgetType {
|
||||
renderedMarkdown?: string;
|
||||
|
||||
constructor(
|
||||
readonly from: number,
|
||||
readonly to: number,
|
||||
readonly from: number | undefined,
|
||||
readonly client: Client,
|
||||
readonly bodyText: string,
|
||||
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||
readonly className: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = "sb-markdown-widget";
|
||||
div.className = this.className;
|
||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||
if (cacheItem) {
|
||||
div.innerHTML = this.wrapHtml(cacheItem.html);
|
||||
div.innerHTML = this.wrapHtml(
|
||||
cacheItem.html,
|
||||
this.from !== undefined,
|
||||
this.from !== undefined,
|
||||
);
|
||||
this.attachListeners(div);
|
||||
}
|
||||
|
||||
@@ -43,6 +47,11 @@ export class MarkdownWidget extends WidgetType {
|
||||
this.bodyText,
|
||||
this.client.currentPage!,
|
||||
);
|
||||
if (!widgetContent) {
|
||||
div.innerHTML = "";
|
||||
// div.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const lang = buildMarkdown(this.client.system.mdExtensions);
|
||||
let mdTree = parse(
|
||||
lang,
|
||||
@@ -80,7 +89,11 @@ export class MarkdownWidget extends WidgetType {
|
||||
// HTML still same as in cache, no need to re-render
|
||||
return;
|
||||
}
|
||||
div.innerHTML = this.wrapHtml(html);
|
||||
div.innerHTML = this.wrapHtml(
|
||||
html,
|
||||
this.from !== undefined,
|
||||
this.from !== undefined,
|
||||
);
|
||||
this.attachListeners(div);
|
||||
|
||||
// Let's give it a tick, then measure and cache
|
||||
@@ -93,12 +106,20 @@ export class MarkdownWidget extends WidgetType {
|
||||
});
|
||||
}
|
||||
|
||||
private wrapHtml(html: string) {
|
||||
private wrapHtml(html: string, editButton = true, sourceButton = true) {
|
||||
return `
|
||||
<div class="button-bar">
|
||||
<button class="source-button" title="Show Markdown source"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg></button>
|
||||
${
|
||||
sourceButton
|
||||
? `<button class="source-button" title="Show Markdown source"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg></button>`
|
||||
: ""
|
||||
}
|
||||
<button class="reload-button" title="Reload"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg></button>
|
||||
<button class="edit-button" title="Edit"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button>
|
||||
${
|
||||
editButton
|
||||
? `<button class="edit-button" title="Edit"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></button>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
${html}`;
|
||||
}
|
||||
@@ -109,7 +130,12 @@ export class MarkdownWidget extends WidgetType {
|
||||
// Override default click behavior with a local navigate (faster)
|
||||
el.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.client.navigate(el.dataset.ref!);
|
||||
const [pageName, pos] = el.dataset.ref!.split(/[$@]/);
|
||||
if (pos && pos.match(/^\d+$/)) {
|
||||
this.client.navigate(pageName, +pos);
|
||||
} else {
|
||||
this.client.navigate(pageName, pos);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,18 +160,20 @@ export class MarkdownWidget extends WidgetType {
|
||||
);
|
||||
});
|
||||
|
||||
div.querySelector(".edit-button")!.addEventListener("click", () => {
|
||||
this.client.editorView.dispatch({
|
||||
selection: { anchor: this.from },
|
||||
if (this.from !== undefined) {
|
||||
div.querySelector(".edit-button")!.addEventListener("click", () => {
|
||||
this.client.editorView.dispatch({
|
||||
selection: { anchor: this.from! },
|
||||
});
|
||||
this.client.focus();
|
||||
});
|
||||
this.client.focus();
|
||||
});
|
||||
div.querySelector(".source-button")!.addEventListener("click", () => {
|
||||
div.innerText = this.renderedMarkdown!;
|
||||
});
|
||||
}
|
||||
div.querySelector(".reload-button")!.addEventListener("click", () => {
|
||||
this.renderContent(div, undefined).catch(console.error);
|
||||
});
|
||||
div.querySelector(".source-button")!.addEventListener("click", () => {
|
||||
div.innerText = this.renderedMarkdown!;
|
||||
});
|
||||
}
|
||||
|
||||
get estimatedHeight(): number {
|
||||
|
||||
@@ -1,67 +1,40 @@
|
||||
import { Decoration, EditorState, WidgetType } from "../deps.ts";
|
||||
import type { Client } from "../client.ts";
|
||||
import { decoratorStateField } from "./util.ts";
|
||||
import { PanelConfig } from "../types.ts";
|
||||
import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts";
|
||||
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||
|
||||
class IFrameWidget extends WidgetType {
|
||||
widgetHeightCacheKey: string;
|
||||
constructor(
|
||||
readonly editor: Client,
|
||||
readonly panel: PanelConfig,
|
||||
readonly className: string,
|
||||
) {
|
||||
super();
|
||||
this.widgetHeightCacheKey = `${this.editor.currentPage!}#${this.className}`;
|
||||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const iframe = createWidgetSandboxIFrame(
|
||||
this.editor,
|
||||
this.widgetHeightCacheKey,
|
||||
this.panel,
|
||||
);
|
||||
iframe.classList.add(this.className);
|
||||
return iframe;
|
||||
}
|
||||
|
||||
get estimatedHeight(): number {
|
||||
return this.editor.space.getCachedWidgetHeight(
|
||||
this.widgetHeightCacheKey,
|
||||
);
|
||||
}
|
||||
|
||||
eq(other: WidgetType): boolean {
|
||||
return this.panel.html ===
|
||||
(other as IFrameWidget).panel.html &&
|
||||
this.panel.script ===
|
||||
(other as IFrameWidget).panel.script;
|
||||
}
|
||||
}
|
||||
|
||||
export function postScriptPrefacePlugin(editor: Client) {
|
||||
export function postScriptPrefacePlugin(
|
||||
editor: Client,
|
||||
) {
|
||||
const panelWidgetHook = editor.system.panelWidgetHook;
|
||||
return decoratorStateField((state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
if (editor.ui.viewState.panels.top.html) {
|
||||
const topCallback = panelWidgetHook.callbacks.get("top");
|
||||
if (topCallback) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new IFrameWidget(
|
||||
widget: new MarkdownWidget(
|
||||
undefined,
|
||||
editor,
|
||||
editor.ui.viewState.panels.top,
|
||||
"sb-top-iframe",
|
||||
`top:${editor.currentPage}`,
|
||||
topCallback,
|
||||
"sb-markdown-top-widget",
|
||||
),
|
||||
side: -1,
|
||||
block: true,
|
||||
}).range(0),
|
||||
);
|
||||
}
|
||||
if (editor.ui.viewState.panels.bottom.html) {
|
||||
const bottomCallback = panelWidgetHook.callbacks.get("bottom");
|
||||
if (bottomCallback) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new IFrameWidget(
|
||||
widget: new MarkdownWidget(
|
||||
undefined,
|
||||
editor,
|
||||
editor.ui.viewState.panels.bottom,
|
||||
"sb-bottom-iframe",
|
||||
`bottom:${editor.currentPage}`,
|
||||
bottomCallback,
|
||||
"sb-markdown-bottom-widget",
|
||||
),
|
||||
side: 1,
|
||||
block: true,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Hook, Manifest } from "../../plugos/types.ts";
|
||||
import { System } from "../../plugos/system.ts";
|
||||
import { CodeWidgetCallback } from "$sb/types.ts";
|
||||
|
||||
export type PanelWidgetT = {
|
||||
panelWidget?: "top" | "bottom";
|
||||
};
|
||||
|
||||
export class PanelWidgetHook implements Hook<PanelWidgetT> {
|
||||
callbacks = new Map<string, CodeWidgetCallback>();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
collectAllPanelWidgets(system: System<PanelWidgetT>) {
|
||||
this.callbacks.clear();
|
||||
for (const plug of system.loadedPlugs.values()) {
|
||||
for (
|
||||
const [name, functionDef] of Object.entries(
|
||||
plug.manifest!.functions,
|
||||
)
|
||||
) {
|
||||
if (!functionDef.panelWidget) {
|
||||
continue;
|
||||
}
|
||||
this.callbacks.set(
|
||||
functionDef.panelWidget,
|
||||
(bodyText, pageName) => {
|
||||
return plug.invoke(name, [bodyText, pageName]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply(system: System<PanelWidgetT>): void {
|
||||
this.collectAllPanelWidgets(system);
|
||||
system.on({
|
||||
plugLoaded: () => {
|
||||
this.collectAllPanelWidgets(system);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<PanelWidgetT>): string[] {
|
||||
const errors = [];
|
||||
for (const functionDef of Object.values(manifest.functions)) {
|
||||
if (!functionDef.panelWidget) {
|
||||
continue;
|
||||
}
|
||||
if (!["top", "bottom"].includes(functionDef.panelWidget)) {
|
||||
errors.push(
|
||||
`Panel widgets must be attached to either 'top' or 'bottom'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
+24
-1
@@ -444,8 +444,31 @@
|
||||
}
|
||||
|
||||
.sb-markdown-widget {
|
||||
overflow-y: scroll;
|
||||
margin: 0 0 -4ch 0;
|
||||
}
|
||||
|
||||
.sb-markdown-top-widget h1,
|
||||
.sb-markdown-bottom-widget h1 {
|
||||
border-top-right-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
margin: 0;
|
||||
padding: 10px !important;
|
||||
background-color: var(--editor-directive-background-color);
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.sb-markdown-top-widget {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sb-markdown-bottom-widget {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sb-markdown-widget,
|
||||
.sb-markdown-top-widget:has(*),
|
||||
.sb-markdown-bottom-widget:has(*) {
|
||||
overflow-y: scroll;
|
||||
border: 1px solid var(--editor-directive-background-color);
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -98,8 +98,6 @@ export const initialViewState: AppViewState = {
|
||||
rhs: {},
|
||||
bhs: {},
|
||||
modal: {},
|
||||
top: {},
|
||||
bottom: {},
|
||||
},
|
||||
allPages: [],
|
||||
commands: new Map(),
|
||||
|
||||
Reference in New Issue
Block a user