mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
VDom 10 (#1206)
* get RefOperations and RefUpdates working. * implement a <canvas> API that can be called using RefOperations * prop to disable rehype/markdown (memory leak)
This commit is contained in:
parent
61d6b4d8eb
commit
42e85aea6f
@ -130,6 +130,8 @@ var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App",
|
||||
vdom.E("div", nil,
|
||||
vdom.E("wave:markdown",
|
||||
vdom.P("text", "*quick vdom application to set background colors*"),
|
||||
vdom.P("scrollable", false),
|
||||
vdom.P("rehype", false),
|
||||
),
|
||||
),
|
||||
vdom.E("div", nil,
|
||||
|
@ -180,6 +180,7 @@ type MarkdownProps = {
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
resolveOpts?: MarkdownResolveOpts;
|
||||
scrollable?: boolean;
|
||||
rehype?: boolean;
|
||||
};
|
||||
|
||||
const Markdown = ({
|
||||
@ -190,6 +191,7 @@ const Markdown = ({
|
||||
className,
|
||||
resolveOpts,
|
||||
scrollable = true,
|
||||
rehype = true,
|
||||
onClickExecute,
|
||||
}: MarkdownProps) => {
|
||||
const textAtomValue = useAtomValueSafe<string>(textAtom);
|
||||
@ -250,17 +252,9 @@ const Markdown = ({
|
||||
}, [showToc, tocRef]);
|
||||
|
||||
text = textAtomValue ?? text;
|
||||
|
||||
const ScrollableMarkdown = () => {
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
ref={contentsOsRef}
|
||||
className="content"
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkAlert, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
|
||||
rehypePlugins={[
|
||||
let rehypePlugins = null;
|
||||
if (rehype) {
|
||||
rehypePlugins = [
|
||||
rehypeRaw,
|
||||
rehypeHighlight,
|
||||
() =>
|
||||
@ -279,7 +273,19 @@ const Markdown = ({
|
||||
tagNames: [...(defaultSchema.tagNames || []), "span"],
|
||||
}),
|
||||
() => rehypeSlug({ prefix: idPrefix }),
|
||||
]}
|
||||
];
|
||||
}
|
||||
|
||||
const ScrollableMarkdown = () => {
|
||||
return (
|
||||
<OverlayScrollbarsComponent
|
||||
ref={contentsOsRef}
|
||||
className="content"
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkAlert, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={markdownComponents}
|
||||
>
|
||||
{text}
|
||||
@ -293,26 +299,7 @@ const Markdown = ({
|
||||
<div className="content non-scrollable">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
rehypeHighlight,
|
||||
() =>
|
||||
rehypeSanitize({
|
||||
...defaultSchema,
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
span: [
|
||||
...(defaultSchema.attributes?.span || []),
|
||||
// Allow all class names starting with `hljs-`.
|
||||
["className", /^hljs-./],
|
||||
// Alternatively, to allow only certain class names:
|
||||
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
|
||||
],
|
||||
},
|
||||
tagNames: [...(defaultSchema.tagNames || []), "span"],
|
||||
}),
|
||||
() => rehypeSlug({ prefix: idPrefix }),
|
||||
]}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={markdownComponents}
|
||||
>
|
||||
{text}
|
||||
|
@ -9,7 +9,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
|
||||
import { applyCanvasOp, mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
import debug from "debug";
|
||||
import * as jotai from "jotai";
|
||||
@ -94,7 +94,7 @@ class VDomWshClient extends WshClient {
|
||||
}
|
||||
|
||||
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
|
||||
console.log("async-initiation", rh.getSource(), data);
|
||||
dlog("async-initiation", rh.getSource(), data);
|
||||
this.model.queueUpdate(true);
|
||||
}
|
||||
}
|
||||
@ -130,6 +130,9 @@ export class VDomModel {
|
||||
persist: jotai.Atom<boolean>;
|
||||
routeGoneUnsub: () => void;
|
||||
routeConfirmed: boolean = false;
|
||||
refOutputStore: Map<string, any> = new Map();
|
||||
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
|
||||
hasBackendWork: boolean = false;
|
||||
|
||||
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
||||
this.viewType = "vdom";
|
||||
@ -201,6 +204,9 @@ export class VDomModel {
|
||||
this.needsImmediateUpdate = false;
|
||||
this.lastUpdateTs = 0;
|
||||
this.queuedUpdate = null;
|
||||
this.refOutputStore.clear();
|
||||
this.globalVersion = jotai.atom(0);
|
||||
this.hasBackendWork = false;
|
||||
globalStore.set(this.contextActive, false);
|
||||
}
|
||||
|
||||
@ -537,6 +543,10 @@ export class VDomModel {
|
||||
this.addErrorMessage(`Could not find ref with id ${refOp.refid}`);
|
||||
continue;
|
||||
}
|
||||
if (elem instanceof HTMLCanvasElement) {
|
||||
applyCanvasOp(elem, refOp, this.refOutputStore);
|
||||
continue;
|
||||
}
|
||||
if (refOp.op == "focus") {
|
||||
if (elem == null) {
|
||||
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: elem is null`);
|
||||
@ -575,7 +585,17 @@ export class VDomModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
globalStore.set(this.globalVersion, globalStore.get(this.globalVersion) + 1);
|
||||
if (update.haswork) {
|
||||
this.hasBackendWork = true;
|
||||
}
|
||||
}
|
||||
|
||||
renderDone(version: number) {
|
||||
// called when the render is done
|
||||
dlog("renderDone", version);
|
||||
if (this.hasRefUpdates() || this.hasBackendWork) {
|
||||
this.hasBackendWork = false;
|
||||
this.queueUpdate(true);
|
||||
}
|
||||
}
|
||||
|
@ -197,3 +197,50 @@ export function mergeBackendUpdates(baseUpdate: VDomBackendUpdate, nextUpdate: V
|
||||
baseUpdate.statesync.push(...nextUpdate.statesync);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCanvasOp(canvas: HTMLCanvasElement, canvasOp: VDomRefOperation, refStore: Map<string, any>) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("Canvas 2D context not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
let { op, params, outputref } = canvasOp;
|
||||
if (params == null) {
|
||||
params = [];
|
||||
}
|
||||
if (op == null || op == "") {
|
||||
return;
|
||||
}
|
||||
// Resolve any reference parameters in params
|
||||
const resolvedParams: any[] = [];
|
||||
params.forEach((param) => {
|
||||
if (typeof param === "string" && param.startsWith("#ref:")) {
|
||||
const refId = param.slice(5); // Remove "#ref:" prefix
|
||||
resolvedParams.push(refStore.get(refId));
|
||||
} else if (typeof param === "string" && param.startsWith("#spreadRef:")) {
|
||||
const refId = param.slice(11); // Remove "#spreadRef:" prefix
|
||||
const arrayRef = refStore.get(refId);
|
||||
if (Array.isArray(arrayRef)) {
|
||||
resolvedParams.push(...arrayRef); // Spread array elements
|
||||
} else {
|
||||
console.error(`Reference ${refId} is not an array and cannot be spread.`);
|
||||
}
|
||||
} else {
|
||||
resolvedParams.push(param);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the operation on the canvas context
|
||||
if (op === "dropRef" && params.length > 0 && typeof params[0] === "string") {
|
||||
refStore.delete(params[0]);
|
||||
} else if (op === "addRef" && outputref) {
|
||||
refStore.set(outputref, resolvedParams[0]);
|
||||
} else if (typeof ctx[op as keyof CanvasRenderingContext2D] === "function") {
|
||||
(ctx[op as keyof CanvasRenderingContext2D] as Function).apply(ctx, resolvedParams);
|
||||
} else if (op in ctx) {
|
||||
(ctx as any)[op] = resolvedParams[0];
|
||||
} else {
|
||||
console.error(`Unsupported canvas operation: ${op}`);
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ const AllowedSimpleTags: { [tagName: string]: boolean } = {
|
||||
br: true,
|
||||
pre: true,
|
||||
code: true,
|
||||
canvas: true,
|
||||
};
|
||||
|
||||
const AllowedSvgTags = {
|
||||
@ -350,7 +351,13 @@ function useVDom(model: VDomModel, elem: VDomElem): GenericPropsType {
|
||||
function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
const props = useVDom(model, elem);
|
||||
return (
|
||||
<Markdown text={props?.text} style={props?.style} className={props?.className} scrollable={props?.scrollable} />
|
||||
<Markdown
|
||||
text={props?.text}
|
||||
style={props?.style}
|
||||
className={props?.className}
|
||||
scrollable={props?.scrollable}
|
||||
rehype={props?.rehype}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -452,11 +459,15 @@ const testVDom: VDomElem = {
|
||||
};
|
||||
|
||||
function VDomRoot({ model }: { model: VDomModel }) {
|
||||
let version = jotai.useAtomValue(model.globalVersion);
|
||||
let rootNode = jotai.useAtomValue(model.vdomRoot);
|
||||
React.useEffect(() => {
|
||||
model.renderDone(version);
|
||||
}, [version]);
|
||||
if (model.viewRef.current == null || rootNode == null) {
|
||||
return null;
|
||||
}
|
||||
dlog("render", rootNode);
|
||||
dlog("render", version, rootNode);
|
||||
let rtn = convertElemToTag(rootNode, model);
|
||||
return <div className="vdom">{rtn}</div>;
|
||||
}
|
||||
|
1
frontend/types/gotypes.d.ts
vendored
1
frontend/types/gotypes.d.ts
vendored
@ -741,6 +741,7 @@ declare global {
|
||||
refid: string;
|
||||
op: string;
|
||||
params?: any[];
|
||||
outputref?: string;
|
||||
};
|
||||
|
||||
// vdom.VDomRefPosition
|
||||
|
@ -346,6 +346,28 @@ func UseId(ctx context.Context) string {
|
||||
return vc.Comp.WaveId
|
||||
}
|
||||
|
||||
func UseRenderTs(ctx context.Context) int64 {
|
||||
vc := getRenderContext(ctx)
|
||||
if vc == nil {
|
||||
panic("UseRenderTs must be called within a component (no context)")
|
||||
}
|
||||
return vc.Root.RenderTs
|
||||
}
|
||||
|
||||
func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) {
|
||||
if ref == nil || !ref.HasCurrent {
|
||||
return
|
||||
}
|
||||
vc := getRenderContext(ctx)
|
||||
if vc == nil {
|
||||
panic("QueueRefOp must be called within a component (no context)")
|
||||
}
|
||||
if op.RefId == "" {
|
||||
op.RefId = ref.RefId
|
||||
}
|
||||
vc.Root.QueueRefOp(op)
|
||||
}
|
||||
|
||||
func depsEqual(deps1 []any, deps2 []any) bool {
|
||||
if len(deps1) != len(deps2) {
|
||||
return false
|
||||
|
@ -6,7 +6,10 @@ package vdom
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
@ -36,11 +39,13 @@ type Atom struct {
|
||||
type RootElem struct {
|
||||
OuterCtx context.Context
|
||||
Root *ComponentImpl
|
||||
RenderTs int64
|
||||
CFuncs map[string]any
|
||||
CompMap map[string]*ComponentImpl // component waveid -> component
|
||||
EffectWorkQueue []*EffectWorkElem
|
||||
NeedsRenderMap map[string]bool
|
||||
Atoms map[string]*Atom
|
||||
RefOperations []VDomRefOperation
|
||||
}
|
||||
|
||||
const (
|
||||
@ -414,6 +419,49 @@ func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentIm
|
||||
r.render(rtnElem, &(*comp).Comp)
|
||||
}
|
||||
|
||||
func (r *RootElem) UpdateRef(updateRef VDomRefUpdate) {
|
||||
refId := updateRef.RefId
|
||||
split := strings.SplitN(refId, ":", 2)
|
||||
if len(split) != 2 {
|
||||
log.Printf("invalid ref id: %s\n", refId)
|
||||
return
|
||||
}
|
||||
waveId := split[0]
|
||||
hookIdx, err := strconv.Atoi(split[1])
|
||||
if err != nil {
|
||||
log.Printf("invalid ref id (bad hook idx): %s\n", refId)
|
||||
return
|
||||
}
|
||||
comp := r.CompMap[waveId]
|
||||
if comp == nil {
|
||||
return
|
||||
}
|
||||
if hookIdx < 0 || hookIdx >= len(comp.Hooks) {
|
||||
return
|
||||
}
|
||||
hook := comp.Hooks[hookIdx]
|
||||
if hook == nil {
|
||||
return
|
||||
}
|
||||
ref, ok := hook.Val.(*VDomRef)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ref.HasCurrent = updateRef.HasCurrent
|
||||
ref.Position = updateRef.Position
|
||||
r.AddRenderWork(waveId)
|
||||
}
|
||||
|
||||
func (r *RootElem) QueueRefOp(op VDomRefOperation) {
|
||||
r.RefOperations = append(r.RefOperations, op)
|
||||
}
|
||||
|
||||
func (r *RootElem) GetRefOperations() []VDomRefOperation {
|
||||
ops := r.RefOperations
|
||||
r.RefOperations = nil
|
||||
return ops
|
||||
}
|
||||
|
||||
func convertPropsToVDom(props map[string]any) map[string]any {
|
||||
if len(props) == 0 {
|
||||
return nil
|
||||
|
@ -189,8 +189,9 @@ type VDomRenderUpdate struct {
|
||||
|
||||
type VDomRefOperation struct {
|
||||
RefId string `json:"refid"`
|
||||
Op string `json:"op" tsype:"\"focus\""`
|
||||
Op string `json:"op"`
|
||||
Params []any `json:"params,omitempty"`
|
||||
OutputRef string `json:"outputref,omitempty"`
|
||||
}
|
||||
|
||||
type VDomMessage struct {
|
||||
|
@ -211,6 +211,7 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
|
||||
RenderUpdates: []vdom.VDomRenderUpdate{
|
||||
{UpdateType: "root", VDom: renderedVDom},
|
||||
},
|
||||
RefOperations: c.Root.GetRefOperations(),
|
||||
StateSync: c.Root.GetStateSync(true),
|
||||
}, nil
|
||||
}
|
||||
@ -228,6 +229,7 @@ func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
|
||||
RenderUpdates: []vdom.VDomRenderUpdate{
|
||||
{UpdateType: "root", VDom: renderedVDom},
|
||||
},
|
||||
RefOperations: c.Root.GetRefOperations(),
|
||||
StateSync: c.Root.GetStateSync(false),
|
||||
}, nil
|
||||
}
|
||||
|
@ -46,6 +46,8 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
|
||||
return respChan
|
||||
}
|
||||
|
||||
impl.Client.Root.RenderTs = feUpdate.Ts
|
||||
|
||||
// set atoms
|
||||
for _, ss := range feUpdate.StateSync {
|
||||
impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
|
||||
@ -60,6 +62,10 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
|
||||
impl.Client.Root.Event(event.WaveId, event.EventType, event)
|
||||
}
|
||||
}
|
||||
// update refs
|
||||
for _, ref := range feUpdate.RefUpdates {
|
||||
impl.Client.Root.UpdateRef(ref)
|
||||
}
|
||||
|
||||
var update *vdom.VDomBackendUpdate
|
||||
var err error
|
||||
|
Loading…
Reference in New Issue
Block a user