Fixed a lot of bugs with new widget rendering

This commit is contained in:
Zef Hemel
2023-12-28 16:14:30 +01:00
parent 4d66f23391
commit d43dbcacec
17 changed files with 212 additions and 142 deletions
+55 -12
View File
@@ -38,7 +38,7 @@ import { OpenPages } from "./open_pages.ts";
import { MainUI } from "./editor_ui.tsx";
import { cleanPageRef } from "$sb/lib/resolve.ts";
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { FileMeta, PageMeta } from "$sb/types.ts";
import { CodeWidgetButton, FileMeta, PageMeta } from "$sb/types.ts";
import { DataStore } from "../plugos/lib/datastore.ts";
import { IndexedDBKvPrimitives } from "../plugos/lib/indexeddb_kv_primitives.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
@@ -188,13 +188,7 @@ export class Client {
// Load settings
this.settings = await ensureSettingsAndIndex(localSpacePrimitives);
// Load widget cache
this.widgetCache = new LimitedMap(
100,
await this.stateDataStore.get(["cache", "widgets"]) ||
{},
);
await this.loadCaches();
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
try {
await this.httpSpacePrimitives.ping();
@@ -484,7 +478,6 @@ export class Client {
this.space = new Space(
localSpacePrimitives,
this.stateDataStore,
this.eventHook,
);
@@ -1013,7 +1006,56 @@ export class Client {
return;
}
private widgetCache = new LimitedMap<WidgetCacheItem>(100);
// Widget and image height caching
private widgetCache = new LimitedMap<WidgetCacheItem>(100); // bodyText -> WidgetCacheItem
private imageHeightCache = new LimitedMap<number>(100); // url -> height
private widgetHeightCache = new LimitedMap<number>(100); // bodytext -> height
async loadCaches() {
const [imageHeightCache, widgetHeightCache, widgetCache] = await this
.stateDataStore.batchGet([["cache", "imageHeight"], [
"cache",
"widgetHeight",
], ["cache", "widgets"]]);
this.imageHeightCache = new LimitedMap(100, imageHeightCache || {});
this.widgetHeightCache = new LimitedMap(100, widgetHeightCache || {});
this.widgetCache = new LimitedMap(100, widgetCache || {});
}
debouncedImageCacheFlush = throttle(() => {
this.stateDataStore.set(["cache", "imageHeight"], this.imageHeightCache)
.catch(
console.error,
);
console.log("Flushed image height cache to store");
}, 5000);
setCachedImageHeight(url: string, height: number) {
this.imageHeightCache.set(url, height);
this.debouncedImageCacheFlush();
}
getCachedImageHeight(url: string): number {
return this.imageHeightCache.get(url) ?? -1;
}
debouncedWidgetHeightCacheFlush = throttle(() => {
this.stateDataStore.set(
["cache", "widgetHeight"],
this.widgetHeightCache.toJSON(),
)
.catch(
console.error,
);
// console.log("Flushed widget height cache to store");
}, 5000);
setCachedWidgetHeight(bodyText: string, height: number) {
this.widgetHeightCache.set(bodyText, height);
this.debouncedWidgetHeightCacheFlush();
}
getCachedWidgetHeight(bodyText: string): number {
return this.widgetHeightCache.get(bodyText) ?? -1;
}
debouncedWidgetCacheFlush = throttle(() => {
this.stateDataStore.set(["cache", "widgets"], this.widgetCache.toJSON())
@@ -1023,8 +1065,8 @@ export class Client {
console.log("Flushed widget cache to store");
}, 5000);
setWidgetCache(key: string, height: number, html: string) {
this.widgetCache.set(key, { height, html });
setWidgetCache(key: string, cacheItem: WidgetCacheItem) {
this.widgetCache.set(key, cacheItem);
this.debouncedWidgetCacheFlush();
}
@@ -1036,4 +1078,5 @@ export class Client {
type WidgetCacheItem = {
height: number;
html: string;
buttons?: CodeWidgetButton[];
};
+1 -1
View File
@@ -57,7 +57,7 @@ export class IFrameWidget extends WidgetType {
}
get estimatedHeight(): number {
const cachedHeight = this.client.space.getCachedWidgetHeight(this.bodyText);
const cachedHeight = this.client.getCachedWidgetHeight(this.bodyText);
// console.log("Calling estimated height", cachedHeight);
return cachedHeight > 0 ? cachedHeight : 150;
}
+3 -3
View File
@@ -25,7 +25,7 @@ class InlineImageWidget extends WidgetType {
}
get estimatedHeight(): number {
const cachedHeight = this.client.space.getCachedImageHeight(this.url);
const cachedHeight = this.client.getCachedImageHeight(this.url);
// console.log("Estimated height requested", this.url, cachedHeight);
return cachedHeight;
}
@@ -35,11 +35,11 @@ class InlineImageWidget extends WidgetType {
let url = this.url;
url = resolvePath(this.client.currentPage!, url, true);
// console.log("Creating DOM", this.url);
const cachedImageHeight = this.client.space.getCachedImageHeight(url);
const cachedImageHeight = this.client.getCachedImageHeight(url);
img.onload = () => {
// console.log("Loaded", this.url, "with height", img.height);
if (img.height !== cachedImageHeight) {
this.client.space.setCachedImageHeight(url, img.height);
this.client.setCachedImageHeight(url, img.height);
}
};
img.src = url;
+67 -42
View File
@@ -1,14 +1,17 @@
import { WidgetType } from "../deps.ts";
import type { Client } from "../client.ts";
import type { CodeWidgetCallback } from "$sb/types.ts";
import type { CodeWidgetButton, CodeWidgetCallback } from "$sb/types.ts";
import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
import { resolveAttachmentPath } from "$sb/lib/resolve.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts";
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { renderToText } from "$sb/lib/tree.ts";
const activeWidgets = new Set<MarkdownWidget>();
export class MarkdownWidget extends WidgetType {
renderedMarkdown?: string;
public dom?: HTMLElement;
constructor(
readonly from: number | undefined,
@@ -27,19 +30,20 @@ export class MarkdownWidget extends WidgetType {
if (cacheItem) {
div.innerHTML = this.wrapHtml(
cacheItem.html,
this.from !== undefined,
this.from !== undefined,
cacheItem.buttons,
);
this.attachListeners(div);
this.attachListeners(div, cacheItem.buttons);
}
// Async kick-off of content renderer
this.renderContent(div, cacheItem?.html).catch(console.error);
this.dom = div;
return div;
}
private async renderContent(
async renderContent(
div: HTMLElement,
cachedHtml: string | undefined,
) {
@@ -47,6 +51,7 @@ export class MarkdownWidget extends WidgetType {
this.bodyText,
this.client.currentPage!,
);
activeWidgets.add(this);
if (!widgetContent) {
div.innerHTML = "";
// div.style.display = "none";
@@ -89,42 +94,30 @@ export class MarkdownWidget extends WidgetType {
// HTML still same as in cache, no need to re-render
return;
}
div.innerHTML = this.wrapHtml(
html,
this.from !== undefined,
this.from !== undefined,
);
this.attachListeners(div);
div.innerHTML = this.wrapHtml(html, widgetContent.buttons);
this.attachListeners(div, widgetContent.buttons);
// Let's give it a tick, then measure and cache
setTimeout(() => {
this.client.setWidgetCache(
this.bodyText,
div.clientHeight,
html,
{ height: div.clientHeight, html, buttons: widgetContent.buttons },
);
});
}
private wrapHtml(html: string, editButton = true, sourceButton = true) {
return `
<div class="button-bar">
${
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>`
: ""
private wrapHtml(html: string, buttons?: CodeWidgetButton[]) {
if (!buttons) {
return html;
}
<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>
${
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}`;
return `<div class="button-bar">${
buttons.map((button, idx) =>
`<button data-button="${idx}" title="${button.description}">${button.svg}</button> `
).join("")
}</div>${html}`;
}
private attachListeners(div: HTMLElement) {
private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) {
div.querySelectorAll("a[data-ref]").forEach((el_) => {
const el = el_ as HTMLElement;
// Override default click behavior with a local navigate (faster)
@@ -160,20 +153,30 @@ export class MarkdownWidget extends WidgetType {
);
});
if (this.from !== undefined) {
div.querySelector(".edit-button")!.addEventListener("click", () => {
this.client.editorView.dispatch({
selection: { anchor: this.from! },
});
this.client.focus();
});
div.querySelector(".source-button")!.addEventListener("click", () => {
div.innerText = this.renderedMarkdown!;
});
if (!buttons) {
buttons = [];
}
div.querySelector(".reload-button")!.addEventListener("click", () => {
this.renderContent(div, undefined).catch(console.error);
});
for (let i = 0; i < buttons.length; i++) {
const button = buttons[i];
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
"click",
() => {
this.client.system.localSyscall("system.invokeFunction", [
button.invokeFunction,
this.from,
]).then((newContent: string | undefined) => {
if (newContent) {
div.innerText = newContent;
}
this.client.focus();
}).catch(console.error);
},
);
}
// div.querySelectorAll("ul > li").forEach((el) => {
// el.classList.add("sb-line-li-1", "sb-line-ul");
// });
}
get estimatedHeight(): number {
@@ -189,3 +192,25 @@ export class MarkdownWidget extends WidgetType {
);
}
}
export function reloadAllMarkdownWidgets() {
for (const widget of activeWidgets) {
// Garbage collect as we go
if (!widget.dom || !widget.dom.parentNode) {
activeWidgets.delete(widget);
continue;
}
widget.renderContent(widget.dom!, undefined).catch(console.error);
}
}
function garbageCollectWidgets() {
for (const widget of activeWidgets) {
if (!widget.dom || !widget.dom.parentNode) {
// console.log("Garbage collecting widget", widget.bodyText);
activeWidgets.delete(widget);
}
}
}
setInterval(garbageCollectWidgets, 5000);
+1 -1
View File
@@ -1,4 +1,4 @@
import { Decoration, EditorState, WidgetType } from "../deps.ts";
import { Decoration, EditorState } from "../deps.ts";
import type { Client } from "../client.ts";
import { decoratorStateField } from "./util.ts";
import { MarkdownWidget } from "./markdown_widget.ts";
+1 -1
View File
@@ -149,7 +149,7 @@ export function mountIFrame(
case "setHeight":
iframe.height = data.height + "px";
if (widgetHeightCacheKey) {
client.space.setCachedWidgetHeight(
client.setCachedWidgetHeight(
widgetHeightCacheKey,
data.height,
);
-46
View File
@@ -11,40 +11,6 @@ import { LimitedMap } from "../common/limited_map.ts";
const pageWatchInterval = 5000;
export class Space {
imageHeightCache = new LimitedMap<number>(100); // url -> height
widgetHeightCache = new LimitedMap<number>(100); // bodytext -> height
debouncedImageCacheFlush = throttle(() => {
this.ds.set(["cache", "imageHeight"], this.imageHeightCache).catch(
console.error,
);
console.log("Flushed image height cache to store");
}, 5000);
setCachedImageHeight(url: string, height: number) {
this.imageHeightCache.set(url, height);
this.debouncedImageCacheFlush();
}
getCachedImageHeight(url: string): number {
return this.imageHeightCache.get(url) ?? -1;
}
debouncedWidgetCacheFlush = throttle(() => {
this.ds.set(["cache", "widgetHeight"], this.widgetHeightCache.toJSON())
.catch(
console.error,
);
// console.log("Flushed widget height cache to store");
}, 5000);
setCachedWidgetHeight(bodyText: string, height: number) {
this.widgetHeightCache.set(bodyText, height);
this.debouncedWidgetCacheFlush();
}
getCachedWidgetHeight(bodyText: string): number {
return this.widgetHeightCache.get(bodyText) ?? -1;
}
// We do watch files in the background to detect changes
// This set of pages should only ever contain 1 page
watchedPages = new Set<string>();
@@ -55,20 +21,8 @@ export class Space {
constructor(
readonly spacePrimitives: SpacePrimitives,
private ds: DataStore,
eventHook: EventHook,
) {
// super();
this.ds.batchGet([["cache", "imageHeight"], ["cache", "widgetHeight"]])
.then(([imageCache, widgetCache]) => {
if (imageCache) {
this.imageHeightCache = new LimitedMap(100, imageCache);
}
if (widgetCache) {
// console.log("Loaded widget cache from store", widgetCache);
this.widgetHeightCache = new LimitedMap(100, widgetCache);
}
});
eventHook.addLocalListener("page:deleted", (pageName: string) => {
if (this.watchedPages.has(pageName)) {
// Stop watching deleted pages already
+9 -12
View File
@@ -451,13 +451,13 @@
.sb-markdown-bottom-widget h1 {
border-top-right-radius: 5px;
border-top-left-radius: 5px;
margin: 0;
margin: 0 0 5px 0;
padding: 10px !important;
background-color: var(--editor-directive-background-color);
font-size: 1.2em;
}
.sb-markdown-top-widget {
.sb-markdown-top-widget:has(*) {
margin-bottom: 10px;
}
@@ -471,7 +471,7 @@
overflow-y: scroll;
border: 1px solid var(--editor-directive-background-color);
border-radius: 5px;
white-space: nowrap;
white-space: wrap;
position: relative;
ul,
@@ -482,7 +482,7 @@
ul {
list-style: none;
padding-left: 1ch;
// padding-left: 1ch;
}
ul li::before {
@@ -493,7 +493,7 @@
/* Needed to add space between the bullet and the text */
width: 1em;
/* Also needed for space (tweak if needed) */
// margin-left: -1em;
margin-left: -1em;
}
h1,
@@ -528,27 +528,24 @@
.button-bar {
position: absolute;
right: 6px;
top: 6px;
right: 10px;
top: 10px;
display: none;
background: var(--editor-directive-background-color);
padding-inline: 3px;
padding-bottom: 1px;
border-radius: 5px;
button {
border: none;
background: none;
cursor: pointer;
color: var(--root-color);
margin-right: -8px;
}
}
.edit-button,
.reload-button {
margin-left: -10px;
}
}
.sb-fenced-code-iframe {
+2
View File
@@ -1,10 +1,12 @@
import { SysCallMapping } from "../../plugos/system.ts";
import { reloadAllMarkdownWidgets } from "../cm_plugins/markdown_widget.ts";
import { broadcastReload } from "../components/widget_sandbox_iframe.ts";
export function clientCodeWidgetSyscalls(): SysCallMapping {
return {
"codeWidget.refreshAll": () => {
broadcastReload();
reloadAllMarkdownWidgets();
},
};
}