Top-bottom panel refactor, more instant rendering
This commit is contained in:
parent
9403fd2cd9
commit
4d66f23391
@ -7,6 +7,7 @@ import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts";
|
|||||||
import { CodeWidgetT } from "../web/hooks/code_widget.ts";
|
import { CodeWidgetT } from "../web/hooks/code_widget.ts";
|
||||||
import { MQHookT } from "../plugos/hooks/mq.ts";
|
import { MQHookT } from "../plugos/hooks/mq.ts";
|
||||||
import { EndpointHookT } from "../plugos/hooks/endpoint.ts";
|
import { EndpointHookT } from "../plugos/hooks/endpoint.ts";
|
||||||
|
import { PanelWidgetT } from "../web/hooks/panel_widget.ts";
|
||||||
|
|
||||||
/** Silverbullet hooks give plugs access to silverbullet core systems.
|
/** Silverbullet hooks give plugs access to silverbullet core systems.
|
||||||
*
|
*
|
||||||
@ -22,6 +23,7 @@ export type SilverBulletHooks =
|
|||||||
& MQHookT
|
& MQHookT
|
||||||
& EventHookT
|
& EventHookT
|
||||||
& CodeWidgetT
|
& CodeWidgetT
|
||||||
|
& PanelWidgetT
|
||||||
& EndpointHookT
|
& EndpointHookT
|
||||||
& PlugNamespaceHookT;
|
& PlugNamespaceHookT;
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ export function filterBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function showPanel(
|
export function showPanel(
|
||||||
id: "lhs" | "rhs" | "bhs" | "modal" | "bottom" | "top",
|
id: "lhs" | "rhs" | "bhs" | "modal",
|
||||||
mode: number,
|
mode: number,
|
||||||
html: string,
|
html: string,
|
||||||
script = "",
|
script = "",
|
||||||
@ -89,7 +89,7 @@ export function showPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hidePanel(
|
export function hidePanel(
|
||||||
id: "lhs" | "rhs" | "bhs" | "modal" | "bottom" | "top",
|
id: "lhs" | "rhs" | "bhs" | "modal",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return syscall("editor.hidePanel", id);
|
return syscall("editor.hidePanel", id);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
function processClick(e) {
|
|
||||||
const dataEl = e.target.closest("[data-ref]");
|
|
||||||
syscall(
|
|
||||||
"system.invokeFunction",
|
|
||||||
"index.navigateToMention",
|
|
||||||
dataEl.getAttribute("data-ref"),
|
|
||||||
).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("link-ul").addEventListener("click", processClick);
|
|
||||||
document.getElementById("hide-button").addEventListener("click", () => {
|
|
||||||
syscall("system.invokeFunction", "index.toggleMentions").catch(console.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("reload-button").addEventListener("click", () => {
|
|
||||||
syscall("system.invokeFunction", "index.renderMentions").catch(
|
|
||||||
console.error,
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,76 +0,0 @@
|
|||||||
/* Reset SB styles */
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
/*height: initial !important;
|
|
||||||
overflow-x: initial !important;
|
|
||||||
overflow-y: hidden !important;*/
|
|
||||||
background-color: var(--root-background-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sb-main {
|
|
||||||
height: initial !important;
|
|
||||||
display: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sb-editor {
|
|
||||||
flex: initial !important;
|
|
||||||
height: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
|
||||||
height: initial !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--ui-font);
|
|
||||||
|
|
||||||
color: var(--root-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-line-h2 {
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px !important;
|
|
||||||
background-color: rgba(233, 233, 233, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#button-bar {
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 10px;
|
|
||||||
padding: 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#button-bar button {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--root-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#edit-button {
|
|
||||||
margin-left: -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
li code {
|
|
||||||
font-size: 80%;
|
|
||||||
color: #a5a4a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.toc-header-1 {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.toc-header-2 {
|
|
||||||
margin-left: 2ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.toc-header-3 {
|
|
||||||
margin-left: 4ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.toc-header-4 {
|
|
||||||
margin-left: 6ch;
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
function processClick(e) {
|
|
||||||
const dataEl = e.target.closest("[data-ref]");
|
|
||||||
syscall(
|
|
||||||
"system.invokeFunction",
|
|
||||||
"index.navigateToMention",
|
|
||||||
dataEl.getAttribute("data-ref"),
|
|
||||||
).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("link-ul").addEventListener("click", processClick);
|
|
||||||
document.getElementById("hide-button").addEventListener("click", () => {
|
|
||||||
syscall("system.invokeFunction", "index.toggleTOC").catch(console.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.addEventListener("mouseenter", () => {
|
|
||||||
console.log("Refreshing on focus");
|
|
||||||
syscall("system.invokeFunction", "index.renderTOC").catch(
|
|
||||||
console.error,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("reload-button").addEventListener("click", () => {
|
|
||||||
syscall("system.invokeFunction", "index.renderTOC").catch(
|
|
||||||
console.error,
|
|
||||||
);
|
|
||||||
});
|
|
@ -10,8 +10,6 @@ syntax:
|
|||||||
- "$"
|
- "$"
|
||||||
regex: "\\$[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*"
|
regex: "\\$[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*"
|
||||||
className: sb-named-anchor
|
className: sb-named-anchor
|
||||||
assets:
|
|
||||||
- asset/*
|
|
||||||
functions:
|
functions:
|
||||||
loadBuiltinsIntoIndex:
|
loadBuiltinsIntoIndex:
|
||||||
path: builtins.ts:loadBuiltinsIntoIndex
|
path: builtins.ts:loadBuiltinsIntoIndex
|
||||||
@ -159,20 +157,14 @@ functions:
|
|||||||
|
|
||||||
# Mentions panel (postscript)
|
# Mentions panel (postscript)
|
||||||
toggleMentions:
|
toggleMentions:
|
||||||
path: "./mentions_ps.ts:toggleMentions"
|
path: "./linked_mentions.ts:toggleMentions"
|
||||||
command:
|
command:
|
||||||
name: "Mentions: Toggle"
|
name: "Mentions: Toggle"
|
||||||
key: ctrl-alt-m
|
key: ctrl-alt-m
|
||||||
updateMentions:
|
|
||||||
path: "./mentions_ps.ts:updateMentions"
|
|
||||||
env: client
|
|
||||||
events:
|
|
||||||
- editor:pageLoaded
|
|
||||||
navigateToMention:
|
|
||||||
path: "./mentions_ps.ts:navigate"
|
|
||||||
|
|
||||||
renderMentions:
|
renderMentions:
|
||||||
path: "./mentions_ps.ts:renderMentions"
|
path: "./linked_mentions.ts:renderMentions"
|
||||||
|
panelWidget: bottom
|
||||||
|
|
||||||
# TOC
|
# TOC
|
||||||
toggleTOC:
|
toggleTOC:
|
||||||
@ -184,8 +176,7 @@ functions:
|
|||||||
renderTOC:
|
renderTOC:
|
||||||
path: toc.ts:renderTOC
|
path: toc.ts:renderTOC
|
||||||
env: client
|
env: client
|
||||||
events:
|
panelWidget: top
|
||||||
- editor:pageLoaded
|
|
||||||
|
|
||||||
lintYAML:
|
lintYAML:
|
||||||
path: lint.ts:lintYAML
|
path: lint.ts:lintYAML
|
||||||
|
46
plugs/index/linked_mentions.ts
Normal file
46
plugs/index/linked_mentions.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { clientStore, editor, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||||
|
import { CodeWidgetContent } from "$sb/types.ts";
|
||||||
|
import { queryObjects } from "./api.ts";
|
||||||
|
import { LinkObject } from "./page_links.ts";
|
||||||
|
|
||||||
|
const hideMentionsKey = "hideMentions";
|
||||||
|
|
||||||
|
export async function toggleMentions() {
|
||||||
|
let hideMentions = await clientStore.get(hideMentionsKey);
|
||||||
|
hideMentions = !hideMentions;
|
||||||
|
await clientStore.set(hideMentionsKey, hideMentions);
|
||||||
|
if (!hideMentions) {
|
||||||
|
await renderMentions();
|
||||||
|
} else {
|
||||||
|
await editor.dispatch({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMentions(): Promise<CodeWidgetContent | null> {
|
||||||
|
const page = await editor.getCurrentPage();
|
||||||
|
const linksResult = await queryObjects<LinkObject>("link", {
|
||||||
|
// Query all links that point to this page, excluding those that are inside directives and self pointers.
|
||||||
|
filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["and", ["=", [
|
||||||
|
"attr",
|
||||||
|
"toPage",
|
||||||
|
], ["string", page]], ["=", ["attr", "inDirective"], ["boolean", false]]]],
|
||||||
|
});
|
||||||
|
if (linksResult.length === 0) {
|
||||||
|
// Don't show the panel if there are no links here.
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
let renderedMd = "# Linked Mentions\n";
|
||||||
|
for (const link of linksResult) {
|
||||||
|
let snippet = await system.invokeFunction(
|
||||||
|
"markdown.markdownToHtml",
|
||||||
|
link.snippet,
|
||||||
|
);
|
||||||
|
// strip HTML tags
|
||||||
|
snippet = snippet.replace(/<[^>]*>?/gm, "");
|
||||||
|
renderedMd += `* [[${link.ref}]]: ...${snippet}...\n`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
markdown: renderedMd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,85 +0,0 @@
|
|||||||
import { asset } from "$sb/plugos-syscall/mod.ts";
|
|
||||||
import { clientStore, editor } from "$sb/silverbullet-syscall/mod.ts";
|
|
||||||
import { queryObjects } from "./api.ts";
|
|
||||||
import { LinkObject } from "./page_links.ts";
|
|
||||||
|
|
||||||
const hideMentionsKey = "hideMentions";
|
|
||||||
|
|
||||||
export async function toggleMentions() {
|
|
||||||
let hideMentions = await clientStore.get(hideMentionsKey);
|
|
||||||
hideMentions = !hideMentions;
|
|
||||||
await clientStore.set(hideMentionsKey, hideMentions);
|
|
||||||
if (!hideMentions) {
|
|
||||||
await renderMentions();
|
|
||||||
} else {
|
|
||||||
await editor.hidePanel("bottom");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Triggered when switching pages or upon first load
|
|
||||||
export async function updateMentions() {
|
|
||||||
if (await clientStore.get(hideMentionsKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await renderMentions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// use internal navigation via syscall to prevent reloading the full page.
|
|
||||||
export async function navigate(ref: string) {
|
|
||||||
const currentPage = await editor.getCurrentPage();
|
|
||||||
const [page, pos] = ref.split(/[@$]/);
|
|
||||||
if (page === currentPage) {
|
|
||||||
await editor.moveCursor(+pos, true);
|
|
||||||
} else {
|
|
||||||
await editor.navigate(page, +pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(unsafe: string) {
|
|
||||||
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(
|
|
||||||
/>/g,
|
|
||||||
">",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderMentions() {
|
|
||||||
const page = await editor.getCurrentPage();
|
|
||||||
const linksResult = await queryObjects<LinkObject>("link", {
|
|
||||||
// Query all links that point to this page, excluding those that are inside directives and self pointers.
|
|
||||||
filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["and", ["=", [
|
|
||||||
"attr",
|
|
||||||
"toPage",
|
|
||||||
], ["string", page]], ["=", ["attr", "inDirective"], ["boolean", false]]]],
|
|
||||||
});
|
|
||||||
if (linksResult.length === 0) {
|
|
||||||
// Don't show the panel if there are no links here.
|
|
||||||
await editor.hidePanel("bottom");
|
|
||||||
} else {
|
|
||||||
const css = await asset.readAsset("asset/style.css");
|
|
||||||
const js = await asset.readAsset("asset/linked_mentions.js");
|
|
||||||
|
|
||||||
await editor.showPanel(
|
|
||||||
"bottom",
|
|
||||||
1,
|
|
||||||
` <style>${css}</style>
|
|
||||||
<div id="sb-main"><div id="sb-editor"><div class="cm-editor">
|
|
||||||
<div id="button-bar">
|
|
||||||
<button id="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 id="hide-button" title="Hide linked mentions"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg></button>
|
|
||||||
</div>
|
|
||||||
<div class="cm-line sb-line-h2">Linked Mentions</div>
|
|
||||||
<ul id="link-ul">
|
|
||||||
${
|
|
||||||
linksResult.map((link) =>
|
|
||||||
`<li data-ref="${link.ref}"><span class="sb-wiki-link-page">${link.ref}</span>: <code>...${
|
|
||||||
escapeHtml(link.snippet)
|
|
||||||
}...</code></li>`
|
|
||||||
).join("")
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div></div></div>
|
|
||||||
`,
|
|
||||||
js,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import {
|
|||||||
system,
|
system,
|
||||||
} from "$sb/silverbullet-syscall/mod.ts";
|
} from "$sb/silverbullet-syscall/mod.ts";
|
||||||
import { renderToText, traverseTree, traverseTreeAsync } from "$sb/lib/tree.ts";
|
import { renderToText, traverseTree, traverseTreeAsync } from "$sb/lib/tree.ts";
|
||||||
import { asset } from "$sb/syscalls.ts";
|
import { CodeWidgetContent } from "$sb/types.ts";
|
||||||
|
|
||||||
const hideTOCKey = "hideTOC";
|
const hideTOCKey = "hideTOC";
|
||||||
const headerThreshold = 3;
|
const headerThreshold = 3;
|
||||||
@ -30,9 +30,11 @@ async function markdownToHtml(text: string): Promise<string> {
|
|||||||
return system.invokeFunction("markdown.markdownToHtml", text);
|
return system.invokeFunction("markdown.markdownToHtml", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderTOC(reload = false) {
|
export async function renderTOC(
|
||||||
|
reload = false,
|
||||||
|
): Promise<CodeWidgetContent | null> {
|
||||||
if (await clientStore.get(hideTOCKey)) {
|
if (await clientStore.get(hideTOCKey)) {
|
||||||
return editor.hidePanel("top");
|
return null;
|
||||||
}
|
}
|
||||||
const page = await editor.getCurrentPage();
|
const page = await editor.getCurrentPage();
|
||||||
const text = await editor.getText();
|
const text = await editor.getText();
|
||||||
@ -41,9 +43,7 @@ export async function renderTOC(reload = false) {
|
|||||||
await traverseTreeAsync(tree, async (n) => {
|
await traverseTreeAsync(tree, async (n) => {
|
||||||
if (n.type?.startsWith("ATXHeading")) {
|
if (n.type?.startsWith("ATXHeading")) {
|
||||||
headers.push({
|
headers.push({
|
||||||
name: await markdownToHtml(
|
name: n.children!.slice(1).map(renderToText).join("").trim(),
|
||||||
n.children!.slice(1).map(renderToText).join("").trim(),
|
|
||||||
),
|
|
||||||
pos: n.from!,
|
pos: n.from!,
|
||||||
level: +n.type[n.type.length - 1],
|
level: +n.type[n.type.length - 1],
|
||||||
});
|
});
|
||||||
@ -52,39 +52,19 @@ export async function renderTOC(reload = false) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
// console.log("All headers", headers);
|
|
||||||
if (!reload && cachedTOC === JSON.stringify(headers)) {
|
|
||||||
// TOC is the same, not updating
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cachedTOC = JSON.stringify(headers);
|
|
||||||
if (headers.length < headerThreshold) {
|
if (headers.length < headerThreshold) {
|
||||||
// console.log("Not enough headers, not showing TOC", headers.length);
|
console.log("Not enough headers, not showing TOC", headers.length);
|
||||||
await editor.hidePanel("top");
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const css = await asset.readAsset("asset/style.css");
|
// console.log("Headers", headers);
|
||||||
const js = await asset.readAsset("asset/toc.js");
|
const renderedMd = "# Table of Contents\n" +
|
||||||
|
headers.map((header) =>
|
||||||
await editor.showPanel(
|
`${
|
||||||
"top",
|
" ".repeat((header.level - 1) * 2)
|
||||||
1,
|
}* [[${page}@${header.pos}|${header.name}]]`
|
||||||
` <style>${css}</style>
|
).join("\n");
|
||||||
<div id="sb-main"><div id="sb-editor"><div class="cm-editor">
|
// console.log("Markdown", renderedMd);
|
||||||
<div id="button-bar">
|
return {
|
||||||
<button id="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>
|
markdown: renderedMd,
|
||||||
<button id="hide-button" title="Hide TOC"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="css-i6dzq1"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg></button>
|
};
|
||||||
</div>
|
|
||||||
<div class="cm-line sb-line-h2">Table of Contents</div>
|
|
||||||
<ul id="link-ul">
|
|
||||||
${
|
|
||||||
headers.map((header) =>
|
|
||||||
`<li data-ref="${page}@${header.pos}" class="toc-header-${header.level}"><span class="sb-wiki-link-page">${header.name}</span></li>`
|
|
||||||
).join("")
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div></div></div>
|
|
||||||
`,
|
|
||||||
js,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import { clientCodeWidgetSyscalls } from "./syscalls/client_code_widget.ts";
|
|||||||
import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts";
|
import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts";
|
||||||
import { deepObjectMerge } from "$sb/lib/json.ts";
|
import { deepObjectMerge } from "$sb/lib/json.ts";
|
||||||
import { Query } from "$sb/types.ts";
|
import { Query } from "$sb/types.ts";
|
||||||
|
import { PanelWidgetHook } from "./hooks/panel_widget.ts";
|
||||||
|
|
||||||
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ export class ClientSystem {
|
|||||||
codeWidgetHook: CodeWidgetHook;
|
codeWidgetHook: CodeWidgetHook;
|
||||||
mdExtensions: MDExt[] = [];
|
mdExtensions: MDExt[] = [];
|
||||||
system: System<SilverBulletHooks>;
|
system: System<SilverBulletHooks>;
|
||||||
|
panelWidgetHook: PanelWidgetHook;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private client: Client,
|
private client: Client,
|
||||||
@ -83,6 +85,10 @@ export class ClientSystem {
|
|||||||
this.codeWidgetHook = new CodeWidgetHook();
|
this.codeWidgetHook = new CodeWidgetHook();
|
||||||
this.system.addHook(this.codeWidgetHook);
|
this.system.addHook(this.codeWidgetHook);
|
||||||
|
|
||||||
|
// Panel widget hook
|
||||||
|
this.panelWidgetHook = new PanelWidgetHook();
|
||||||
|
this.system.addHook(this.panelWidgetHook);
|
||||||
|
|
||||||
// MQ hook
|
// MQ hook
|
||||||
if (client.syncMode) {
|
if (client.syncMode) {
|
||||||
// Process MQ messages locally
|
// Process MQ messages locally
|
||||||
|
@ -70,10 +70,10 @@ export function fencedCodePlugin(editor: Client) {
|
|||||||
const widget = renderMode === "markdown"
|
const widget = renderMode === "markdown"
|
||||||
? new MarkdownWidget(
|
? new MarkdownWidget(
|
||||||
from + lineStrings[0].length + 1,
|
from + lineStrings[0].length + 1,
|
||||||
to - lineStrings[lineStrings.length - 1].length - 1,
|
|
||||||
editor,
|
editor,
|
||||||
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
||||||
codeWidgetCallback,
|
codeWidgetCallback,
|
||||||
|
"sb-markdown-widget",
|
||||||
)
|
)
|
||||||
: new IFrameWidget(
|
: new IFrameWidget(
|
||||||
from + lineStrings[0].length + 1,
|
from + lineStrings[0].length + 1,
|
||||||
|
@ -11,21 +11,25 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
renderedMarkdown?: string;
|
renderedMarkdown?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly from: number,
|
readonly from: number | undefined,
|
||||||
readonly to: number,
|
|
||||||
readonly client: Client,
|
readonly client: Client,
|
||||||
readonly bodyText: string,
|
readonly bodyText: string,
|
||||||
readonly codeWidgetCallback: CodeWidgetCallback,
|
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||||
|
readonly className: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOM(): HTMLElement {
|
toDOM(): HTMLElement {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "sb-markdown-widget";
|
div.className = this.className;
|
||||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||||
if (cacheItem) {
|
if (cacheItem) {
|
||||||
div.innerHTML = this.wrapHtml(cacheItem.html);
|
div.innerHTML = this.wrapHtml(
|
||||||
|
cacheItem.html,
|
||||||
|
this.from !== undefined,
|
||||||
|
this.from !== undefined,
|
||||||
|
);
|
||||||
this.attachListeners(div);
|
this.attachListeners(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +47,11 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
this.bodyText,
|
this.bodyText,
|
||||||
this.client.currentPage!,
|
this.client.currentPage!,
|
||||||
);
|
);
|
||||||
|
if (!widgetContent) {
|
||||||
|
div.innerHTML = "";
|
||||||
|
// div.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const lang = buildMarkdown(this.client.system.mdExtensions);
|
const lang = buildMarkdown(this.client.system.mdExtensions);
|
||||||
let mdTree = parse(
|
let mdTree = parse(
|
||||||
lang,
|
lang,
|
||||||
@ -80,7 +89,11 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
// HTML still same as in cache, no need to re-render
|
// HTML still same as in cache, no need to re-render
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
div.innerHTML = this.wrapHtml(html);
|
div.innerHTML = this.wrapHtml(
|
||||||
|
html,
|
||||||
|
this.from !== undefined,
|
||||||
|
this.from !== undefined,
|
||||||
|
);
|
||||||
this.attachListeners(div);
|
this.attachListeners(div);
|
||||||
|
|
||||||
// Let's give it a tick, then measure and cache
|
// 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 `
|
return `
|
||||||
<div class="button-bar">
|
<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="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>
|
</div>
|
||||||
${html}`;
|
${html}`;
|
||||||
}
|
}
|
||||||
@ -109,7 +130,12 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
// Override default click behavior with a local navigate (faster)
|
// Override default click behavior with a local navigate (faster)
|
||||||
el.addEventListener("click", (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
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", () => {
|
if (this.from !== undefined) {
|
||||||
this.client.editorView.dispatch({
|
div.querySelector(".edit-button")!.addEventListener("click", () => {
|
||||||
selection: { anchor: this.from },
|
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", () => {
|
div.querySelector(".reload-button")!.addEventListener("click", () => {
|
||||||
this.renderContent(div, undefined).catch(console.error);
|
this.renderContent(div, undefined).catch(console.error);
|
||||||
});
|
});
|
||||||
div.querySelector(".source-button")!.addEventListener("click", () => {
|
|
||||||
div.innerText = this.renderedMarkdown!;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get estimatedHeight(): number {
|
get estimatedHeight(): number {
|
||||||
|
@ -1,67 +1,40 @@
|
|||||||
import { Decoration, EditorState, WidgetType } from "../deps.ts";
|
import { Decoration, EditorState, WidgetType } from "../deps.ts";
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../client.ts";
|
||||||
import { decoratorStateField } from "./util.ts";
|
import { decoratorStateField } from "./util.ts";
|
||||||
import { PanelConfig } from "../types.ts";
|
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||||
import { createWidgetSandboxIFrame } from "../components/widget_sandbox_iframe.ts";
|
|
||||||
|
|
||||||
class IFrameWidget extends WidgetType {
|
export function postScriptPrefacePlugin(
|
||||||
widgetHeightCacheKey: string;
|
editor: Client,
|
||||||
constructor(
|
) {
|
||||||
readonly editor: Client,
|
const panelWidgetHook = editor.system.panelWidgetHook;
|
||||||
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) {
|
|
||||||
return decoratorStateField((state: EditorState) => {
|
return decoratorStateField((state: EditorState) => {
|
||||||
const widgets: any[] = [];
|
const widgets: any[] = [];
|
||||||
if (editor.ui.viewState.panels.top.html) {
|
const topCallback = panelWidgetHook.callbacks.get("top");
|
||||||
|
if (topCallback) {
|
||||||
widgets.push(
|
widgets.push(
|
||||||
Decoration.widget({
|
Decoration.widget({
|
||||||
widget: new IFrameWidget(
|
widget: new MarkdownWidget(
|
||||||
|
undefined,
|
||||||
editor,
|
editor,
|
||||||
editor.ui.viewState.panels.top,
|
`top:${editor.currentPage}`,
|
||||||
"sb-top-iframe",
|
topCallback,
|
||||||
|
"sb-markdown-top-widget",
|
||||||
),
|
),
|
||||||
side: -1,
|
side: -1,
|
||||||
block: true,
|
block: true,
|
||||||
}).range(0),
|
}).range(0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (editor.ui.viewState.panels.bottom.html) {
|
const bottomCallback = panelWidgetHook.callbacks.get("bottom");
|
||||||
|
if (bottomCallback) {
|
||||||
widgets.push(
|
widgets.push(
|
||||||
Decoration.widget({
|
Decoration.widget({
|
||||||
widget: new IFrameWidget(
|
widget: new MarkdownWidget(
|
||||||
|
undefined,
|
||||||
editor,
|
editor,
|
||||||
editor.ui.viewState.panels.bottom,
|
`bottom:${editor.currentPage}`,
|
||||||
"sb-bottom-iframe",
|
bottomCallback,
|
||||||
|
"sb-markdown-bottom-widget",
|
||||||
),
|
),
|
||||||
side: 1,
|
side: 1,
|
||||||
block: true,
|
block: true,
|
||||||
|
59
web/hooks/panel_widget.ts
Normal file
59
web/hooks/panel_widget.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -444,8 +444,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sb-markdown-widget {
|
.sb-markdown-widget {
|
||||||
overflow-y: scroll;
|
|
||||||
margin: 0 0 -4ch 0;
|
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: 1px solid var(--editor-directive-background-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -98,8 +98,6 @@ export const initialViewState: AppViewState = {
|
|||||||
rhs: {},
|
rhs: {},
|
||||||
bhs: {},
|
bhs: {},
|
||||||
modal: {},
|
modal: {},
|
||||||
top: {},
|
|
||||||
bottom: {},
|
|
||||||
},
|
},
|
||||||
allPages: [],
|
allPages: [],
|
||||||
commands: new Map(),
|
commands: new Map(),
|
||||||
|
Loading…
Reference in New Issue
Block a user