mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
vdom 3 (#1033)
This commit is contained in:
parent
c1c90bb4f8
commit
46783ba315
@ -29,6 +29,7 @@ func GenerateWshClient() error {
|
|||||||
"github.com/wavetermdev/waveterm/pkg/waveobj",
|
"github.com/wavetermdev/waveterm/pkg/waveobj",
|
||||||
"github.com/wavetermdev/waveterm/pkg/wconfig",
|
"github.com/wavetermdev/waveterm/pkg/wconfig",
|
||||||
"github.com/wavetermdev/waveterm/pkg/wps",
|
"github.com/wavetermdev/waveterm/pkg/wps",
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom",
|
||||||
})
|
})
|
||||||
wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
|
wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
|
||||||
for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {
|
for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func Page(ctx context.Context, props map[string]any) any {
|
func Page(ctx context.Context, props map[string]any) any {
|
||||||
clicked, setClicked := vdom.UseState(ctx, false)
|
clicked, setClicked := vdom.UseState(ctx, false)
|
||||||
var clickedDiv *vdom.Elem
|
var clickedDiv *vdom.VDomElem
|
||||||
if clicked {
|
if clicked {
|
||||||
clickedDiv = vdom.Bind(`<div>clicked</div>`, nil)
|
clickedDiv = vdom.Bind(`<div>clicked</div>`, nil)
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ func Page(ctx context.Context, props map[string]any) any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Button(ctx context.Context, props map[string]any) any {
|
func Button(ctx context.Context, props map[string]any) any {
|
||||||
ref := vdom.UseRef(ctx, nil)
|
ref := vdom.UseVDomRef(ctx)
|
||||||
clName, setClName := vdom.UseState(ctx, "button")
|
clName, setClName := vdom.UseState(ctx, "button")
|
||||||
vdom.UseEffect(ctx, func() func() {
|
vdom.UseEffect(ctx, func() func() {
|
||||||
fmt.Printf("Button useEffect\n")
|
fmt.Printf("Button useEffect\n")
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,29 +18,61 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var htmlCmd = &cobra.Command{
|
var htmlCmd = &cobra.Command{
|
||||||
Use: "html",
|
Use: "html",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Short: "Launch a demo html-mode terminal",
|
Short: "launch demo vdom application",
|
||||||
Run: htmlRun,
|
RunE: htmlRun,
|
||||||
PreRunE: preRunSetupRpcClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func htmlRun(cmd *cobra.Command, args []string) {
|
func MakeVDom() *vdom.VDomElem {
|
||||||
defer wshutil.DoShutdown("normal exit", 0, true)
|
vdomStr := `
|
||||||
setTermHtmlMode()
|
<div>
|
||||||
for {
|
<h1 style="color:red; background-color: #bind:$.bgcolor; border-radius: 4px; padding: 5px;">hello vdom world</h1>
|
||||||
var buf [1]byte
|
<div><bind key="$.text"/> | num[<bind key="$.num"/>]</div>
|
||||||
_, err := WrappedStdin.Read(buf[:])
|
<div>
|
||||||
if err != nil {
|
<button onClick="#globalevent:clickinc">increment</button>
|
||||||
wshutil.DoShutdown(fmt.Sprintf("stdin closed/error (%v)", err), 1, true)
|
</div>
|
||||||
}
|
</div>
|
||||||
if buf[0] == 0x03 {
|
`
|
||||||
wshutil.DoShutdown("read Ctrl-C from stdin", 1, true)
|
elem := vdom.Bind(vdomStr, nil)
|
||||||
break
|
return elem
|
||||||
}
|
}
|
||||||
if buf[0] == 'x' {
|
|
||||||
wshutil.DoShutdown("read 'x' from stdin", 0, true)
|
func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) {
|
||||||
break
|
if event.PropName == "clickinc" {
|
||||||
}
|
client.SetAtomVal("num", client.GetAtomVal("num").(int)+1)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func htmlRun(cmd *cobra.Command, args []string) error {
|
||||||
|
WriteStderr("running wsh html %q\n", RpcContext.BlockId)
|
||||||
|
|
||||||
|
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client.SetGlobalEventHandler(GlobalEventHandler)
|
||||||
|
log.Printf("created client: %v\n", client)
|
||||||
|
client.SetAtomVal("bgcolor", "#0000ff77")
|
||||||
|
client.SetAtomVal("text", "initial text")
|
||||||
|
client.SetAtomVal("num", 0)
|
||||||
|
client.SetRootElem(MakeVDom())
|
||||||
|
err = client.CreateVDomContext()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("created context\n")
|
||||||
|
go func() {
|
||||||
|
<-client.DoneCh
|
||||||
|
wshutil.DoShutdown("vdom closed by FE", 0, true)
|
||||||
|
}()
|
||||||
|
log.Printf("created vdom context\n")
|
||||||
|
go func() {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
client.SetAtomVal("text", "updated text")
|
||||||
|
client.SendAsyncInitiation()
|
||||||
|
}()
|
||||||
|
<-client.DoneCh
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -36,7 +36,7 @@ type FullBlockProps = {
|
|||||||
|
|
||||||
function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel): ViewModel {
|
function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel): ViewModel {
|
||||||
if (blockView === "term") {
|
if (blockView === "term") {
|
||||||
return makeTerminalModel(blockId);
|
return makeTerminalModel(blockId, nodeModel);
|
||||||
}
|
}
|
||||||
if (blockView === "preview") {
|
if (blockView === "preview") {
|
||||||
return makePreviewModel(blockId, nodeModel);
|
return makePreviewModel(blockId, nodeModel);
|
||||||
@ -253,7 +253,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
|||||||
|
|
||||||
const Block = memo((props: BlockProps) => {
|
const Block = memo((props: BlockProps) => {
|
||||||
counterInc("render-Block");
|
counterInc("render-Block");
|
||||||
counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8));
|
counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8));
|
||||||
const [blockData, loading] = useWaveObjectValue<Block>(makeORef("block", props.nodeModel.blockId));
|
const [blockData, loading] = useWaveObjectValue<Block>(makeORef("block", props.nodeModel.blockId));
|
||||||
const bcm = getBlockComponentModel(props.nodeModel.blockId);
|
const bcm = getBlockComponentModel(props.nodeModel.blockId);
|
||||||
let viewModel = bcm?.viewModel;
|
let viewModel = bcm?.viewModel;
|
||||||
@ -264,6 +264,7 @@ const Block = memo((props: BlockProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
unregisterBlockComponentModel(props.nodeModel.blockId);
|
unregisterBlockComponentModel(props.nodeModel.blockId);
|
||||||
|
viewModel?.dispose?.();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
if (loading || isBlank(props.nodeModel.blockId) || blockData == null) {
|
if (loading || isBlank(props.nodeModel.blockId) || blockData == null) {
|
||||||
|
@ -18,6 +18,10 @@ class RpcResponseHelper {
|
|||||||
this.done = cmdMsg.reqid == null;
|
this.done = cmdMsg.reqid == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSource(): string {
|
||||||
|
return this.cmdMsg?.source;
|
||||||
|
}
|
||||||
|
|
||||||
sendResponse(msg: RpcMessage) {
|
sendResponse(msg: RpcMessage) {
|
||||||
if (this.done || util.isBlank(this.cmdMsg.reqid)) {
|
if (this.done || util.isBlank(this.cmdMsg.reqid)) {
|
||||||
return;
|
return;
|
||||||
|
@ -217,6 +217,21 @@ class RpcApiType {
|
|||||||
return client.wshRpcCall("test", data, opts);
|
return client.wshRpcCall("test", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "vdomasyncinitiation" [call]
|
||||||
|
VDomAsyncInitiationCommand(client: WshClient, data: VDomAsyncInitiationRequest, opts?: RpcOpts): Promise<void> {
|
||||||
|
return client.wshRpcCall("vdomasyncinitiation", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "vdomcreatecontext" [call]
|
||||||
|
VDomCreateContextCommand(client: WshClient, data: VDomCreateContext, opts?: RpcOpts): Promise<void> {
|
||||||
|
return client.wshRpcCall("vdomcreatecontext", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "vdomrender" [call]
|
||||||
|
VDomRenderCommand(client: WshClient, data: VDomFrontendUpdate, opts?: RpcOpts): Promise<VDomBackendUpdate> {
|
||||||
|
return client.wshRpcCall("vdomrender", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
// command "webselector" [call]
|
// command "webselector" [call]
|
||||||
WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise<string[]> {
|
WebSelectorCommand(client: WshClient, data: CommandWebSelectorData, opts?: RpcOpts): Promise<string[]> {
|
||||||
return client.wshRpcCall("webselector", data, opts);
|
return client.wshRpcCall("webselector", data, opts);
|
||||||
|
52
frontend/app/view/term/term-wsh.tsx
Normal file
52
frontend/app/view/term/term-wsh.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { WOS } from "@/app/store/global";
|
||||||
|
import { waveEventSubscribe } from "@/app/store/wps";
|
||||||
|
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||||
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
|
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||||
|
import { TermViewModel } from "@/app/view/term/term";
|
||||||
|
import debug from "debug";
|
||||||
|
|
||||||
|
const dlog = debug("wave:vdom");
|
||||||
|
|
||||||
|
export class TermWshClient extends WshClient {
|
||||||
|
blockId: string;
|
||||||
|
model: TermViewModel;
|
||||||
|
|
||||||
|
constructor(blockId: string, model: TermViewModel) {
|
||||||
|
super(makeFeBlockRouteId(blockId));
|
||||||
|
this.blockId = blockId;
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_vdomcreatecontext(rh: RpcResponseHelper, data: VDomCreateContext) {
|
||||||
|
console.log("vdom-create", rh.getSource(), data);
|
||||||
|
this.model.vdomModel.reset();
|
||||||
|
this.model.vdomModel.backendRoute = rh.getSource();
|
||||||
|
if (!data.persist) {
|
||||||
|
const unsubFn = waveEventSubscribe({
|
||||||
|
eventType: "route:gone",
|
||||||
|
scope: rh.getSource(),
|
||||||
|
handler: () => {
|
||||||
|
RpcApi.SetMetaCommand(this, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "term:mode": null },
|
||||||
|
});
|
||||||
|
unsubFn();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RpcApi.SetMetaCommand(this, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "term:mode": "html" },
|
||||||
|
});
|
||||||
|
this.model.vdomModel.queueUpdate(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
|
||||||
|
console.log("async-initiation", rh.getSource(), data);
|
||||||
|
this.model.vdomModel.queueUpdate(true);
|
||||||
|
}
|
||||||
|
}
|
@ -4,12 +4,15 @@
|
|||||||
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
|
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
|
||||||
import { waveEventSubscribe } from "@/app/store/wps";
|
import { waveEventSubscribe } from "@/app/store/wps";
|
||||||
import { RpcApi } from "@/app/store/wshclientapi";
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||||
|
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
|
||||||
|
import { TermWshClient } from "@/app/view/term/term-wsh";
|
||||||
import { VDomView } from "@/app/view/term/vdom";
|
import { VDomView } from "@/app/view/term/vdom";
|
||||||
|
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||||
|
import { NodeModel } from "@/layout/index";
|
||||||
import { WOS, atoms, getConnStatusAtom, getSettingsKeyAtom, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
import { WOS, atoms, getConnStatusAtom, getSettingsKeyAtom, globalStore, useSettingsPrefixAtom } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as keyutil from "@/util/keyutil";
|
import * as keyutil from "@/util/keyutil";
|
||||||
import * as util from "@/util/util";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@ -19,102 +22,35 @@ import { computeTheme } from "./termutil";
|
|||||||
import { TermWrap } from "./termwrap";
|
import { TermWrap } from "./termwrap";
|
||||||
import "./xterm.css";
|
import "./xterm.css";
|
||||||
|
|
||||||
const keyMap = {
|
|
||||||
Enter: "\r",
|
|
||||||
Backspace: "\x7f",
|
|
||||||
Tab: "\t",
|
|
||||||
Escape: "\x1b",
|
|
||||||
ArrowUp: "\x1b[A",
|
|
||||||
ArrowDown: "\x1b[B",
|
|
||||||
ArrowRight: "\x1b[C",
|
|
||||||
ArrowLeft: "\x1b[D",
|
|
||||||
Insert: "\x1b[2~",
|
|
||||||
Delete: "\x1b[3~",
|
|
||||||
Home: "\x1b[1~",
|
|
||||||
End: "\x1b[4~",
|
|
||||||
PageUp: "\x1b[5~",
|
|
||||||
PageDown: "\x1b[6~",
|
|
||||||
};
|
|
||||||
|
|
||||||
function keyboardEventToASCII(event: React.KeyboardEvent<HTMLInputElement>): string {
|
|
||||||
// check modifiers
|
|
||||||
// if no modifiers are set, just send the key
|
|
||||||
if (!event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
||||||
if (event.key == null || event.key == "") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (keyMap[event.key] != null) {
|
|
||||||
return keyMap[event.key];
|
|
||||||
}
|
|
||||||
if (event.key.length == 1) {
|
|
||||||
return event.key;
|
|
||||||
} else {
|
|
||||||
console.log("not sending keyboard event", event.key, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if meta or alt is set, there is no ASCII representation
|
|
||||||
if (event.metaKey || event.altKey) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
// if ctrl is set, if it is a letter, subtract 64 from the uppercase value to get the ASCII value
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
if (
|
|
||||||
(event.key.length === 1 && event.key >= "A" && event.key <= "Z") ||
|
|
||||||
(event.key >= "a" && event.key <= "z")
|
|
||||||
) {
|
|
||||||
const key = event.key.toUpperCase();
|
|
||||||
return String.fromCharCode(key.charCodeAt(0) - 64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitialLoadDataType = {
|
type InitialLoadDataType = {
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
heldData: Uint8Array[];
|
heldData: Uint8Array[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function vdomText(text: string): VDomElem {
|
|
||||||
return {
|
|
||||||
tag: "#text",
|
|
||||||
text: text,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const testVDom: VDomElem = {
|
|
||||||
id: "testid1",
|
|
||||||
tag: "div",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: "testh1",
|
|
||||||
tag: "h1",
|
|
||||||
children: [vdomText("Hello World")],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "testp",
|
|
||||||
tag: "p",
|
|
||||||
children: [vdomText("This is a paragraph (from VDOM)")],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
class TermViewModel {
|
class TermViewModel {
|
||||||
viewType: string;
|
viewType: string;
|
||||||
|
nodeModel: NodeModel;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
termRef: React.RefObject<TermWrap>;
|
termRef: React.RefObject<TermWrap>;
|
||||||
blockAtom: jotai.Atom<Block>;
|
blockAtom: jotai.Atom<Block>;
|
||||||
termMode: jotai.Atom<string>;
|
termMode: jotai.Atom<string>;
|
||||||
htmlElemFocusRef: React.RefObject<HTMLInputElement>;
|
|
||||||
blockId: string;
|
blockId: string;
|
||||||
viewIcon: jotai.Atom<string>;
|
viewIcon: jotai.Atom<string>;
|
||||||
viewName: jotai.Atom<string>;
|
viewName: jotai.Atom<string>;
|
||||||
blockBg: jotai.Atom<MetaType>;
|
blockBg: jotai.Atom<MetaType>;
|
||||||
manageConnection: jotai.Atom<boolean>;
|
manageConnection: jotai.Atom<boolean>;
|
||||||
connStatus: jotai.Atom<ConnStatus>;
|
connStatus: jotai.Atom<ConnStatus>;
|
||||||
|
termWshClient: TermWshClient;
|
||||||
|
shellProcStatusRef: React.MutableRefObject<string>;
|
||||||
|
vdomModel: VDomModel;
|
||||||
|
|
||||||
constructor(blockId: string) {
|
constructor(blockId: string, nodeModel: NodeModel) {
|
||||||
this.viewType = "term";
|
this.viewType = "term";
|
||||||
this.blockId = blockId;
|
this.blockId = blockId;
|
||||||
|
this.termWshClient = new TermWshClient(blockId, this);
|
||||||
|
DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.termWshClient);
|
||||||
|
this.nodeModel = nodeModel;
|
||||||
|
this.vdomModel = new VDomModel(blockId, nodeModel, null, this.termWshClient);
|
||||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||||
this.termMode = jotai.atom((get) => {
|
this.termMode = jotai.atom((get) => {
|
||||||
const blockData = get(this.blockAtom);
|
const blockData = get(this.blockAtom);
|
||||||
@ -152,6 +88,10 @@ class TermViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
|
||||||
|
}
|
||||||
|
|
||||||
giveFocus(): boolean {
|
giveFocus(): boolean {
|
||||||
let termMode = globalStore.get(this.termMode);
|
let termMode = globalStore.get(this.termMode);
|
||||||
if (termMode == "term") {
|
if (termMode == "term") {
|
||||||
@ -159,15 +99,70 @@ class TermViewModel {
|
|||||||
this.termRef.current.terminal.focus();
|
this.termRef.current.terminal.focus();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (this.htmlElemFocusRef?.current) {
|
|
||||||
this.htmlElemFocusRef.current.focus();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyDownHandler(waveEvent: WaveKeyboardEvent): boolean {
|
||||||
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||||
|
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${this.blockId}`);
|
||||||
|
const blockData = globalStore.get(blockAtom);
|
||||||
|
const newTermMode = blockData?.meta?.["term:mode"] == "html" ? null : "html";
|
||||||
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||||
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
|
meta: { "term:mode": newTermMode },
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const blockData = globalStore.get(this.blockAtom);
|
||||||
|
if (blockData.meta?.["term:mode"] == "html") {
|
||||||
|
return this.vdomModel?.globalKeydownHandler(waveEvent);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTerminalKeydown(event: KeyboardEvent): boolean {
|
||||||
|
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||||
|
if (waveEvent.type != "keydown") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.keyDownHandler(waveEvent)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// deal with terminal specific keybindings
|
||||||
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
|
||||||
|
const p = navigator.clipboard.readText();
|
||||||
|
p.then((text) => {
|
||||||
|
this.termRef.current?.terminal.paste(text);
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
|
||||||
|
const sel = this.termRef.current?.terminal.getSelection();
|
||||||
|
navigator.clipboard.writeText(sel);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||||
|
// restart
|
||||||
|
const tabId = globalStore.get(atoms.staticTabId);
|
||||||
|
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, { tabid: tabId, blockid: this.blockId });
|
||||||
|
prtn.catch((e) => console.log("error controller resync (enter)", this.blockId, e));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const globalKeys = getAllGlobalKeyBindings();
|
||||||
|
for (const key of globalKeys) {
|
||||||
|
if (keyutil.checkKeyPressed(waveEvent, key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
setTerminalTheme(themeName: string) {
|
setTerminalTheme(themeName: string) {
|
||||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||||
oref: WOS.makeORef("block", this.blockId),
|
oref: WOS.makeORef("block", this.blockId),
|
||||||
@ -215,8 +210,8 @@ class TermViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTerminalModel(blockId: string): TermViewModel {
|
function makeTerminalModel(blockId: string, nodeModel: NodeModel): TermViewModel {
|
||||||
return new TermViewModel(blockId);
|
return new TermViewModel(blockId, nodeModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TerminalViewProps {
|
interface TerminalViewProps {
|
||||||
@ -247,63 +242,22 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
||||||
const viewRef = React.createRef<HTMLDivElement>();
|
const viewRef = React.useRef<HTMLDivElement>(null);
|
||||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||||
const termRef = React.useRef<TermWrap>(null);
|
const termRef = React.useRef<TermWrap>(null);
|
||||||
model.termRef = termRef;
|
model.termRef = termRef;
|
||||||
const shellProcStatusRef = React.useRef<string>(null);
|
const spstatusRef = React.useRef<string>(null);
|
||||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
model.shellProcStatusRef = spstatusRef;
|
||||||
model.htmlElemFocusRef = htmlElemFocusRef;
|
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
const termSettingsAtom = useSettingsPrefixAtom("term");
|
const termSettingsAtom = useSettingsPrefixAtom("term");
|
||||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||||
|
let termMode = blockData?.meta?.["term:mode"] ?? "term";
|
||||||
|
if (termMode != "term" && termMode != "html") {
|
||||||
|
termMode = "term";
|
||||||
|
}
|
||||||
|
const termModeRef = React.useRef(termMode);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleTerminalKeydown(event: KeyboardEvent): boolean {
|
|
||||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
|
||||||
if (waveEvent.type != "keydown") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// deal with terminal specific keybindings
|
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
||||||
oref: WOS.makeORef("block", blockId),
|
|
||||||
meta: { "term:mode": null },
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
|
|
||||||
const p = navigator.clipboard.readText();
|
|
||||||
p.then((text) => {
|
|
||||||
termRef.current?.terminal.paste(text);
|
|
||||||
});
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
|
|
||||||
const sel = termRef.current?.terminal.getSelection();
|
|
||||||
navigator.clipboard.writeText(sel);
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
|
||||||
// restart
|
|
||||||
const tabId = globalStore.get(atoms.staticTabId);
|
|
||||||
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, { tabid: tabId, blockid: blockId });
|
|
||||||
prtn.catch((e) => console.log("error controller resync (enter)", blockId, e));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const globalKeys = getAllGlobalKeyBindings();
|
|
||||||
for (const key of globalKeys) {
|
|
||||||
if (keyutil.checkKeyPressed(waveEvent, key)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
||||||
const termTheme = computeTheme(fullConfig, blockData?.meta?.["term:theme"]);
|
const termTheme = computeTheme(fullConfig, blockData?.meta?.["term:theme"]);
|
||||||
const themeCopy = { ...termTheme };
|
const themeCopy = { ...termTheme };
|
||||||
@ -335,7 +289,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
scrollback: termScrollback,
|
scrollback: termScrollback,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keydownHandler: handleTerminalKeydown,
|
keydownHandler: model.handleTerminalKeydown.bind(model),
|
||||||
useWebGl: !termSettings?.["term:disablewebgl"],
|
useWebGl: !termSettings?.["term:disablewebgl"],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -352,29 +306,13 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
};
|
};
|
||||||
}, [blockId, termSettings]);
|
}, [blockId, termSettings]);
|
||||||
|
|
||||||
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
React.useEffect(() => {
|
||||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
if (termModeRef.current == "html" && termMode == "term") {
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
// focus the terminal
|
||||||
// reset term:mode
|
model.giveFocus();
|
||||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
||||||
oref: WOS.makeORef("block", blockId),
|
|
||||||
meta: { "term:mode": null },
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const asciiVal = keyboardEventToASCII(event);
|
termModeRef.current = termMode;
|
||||||
if (asciiVal.length == 0) {
|
}, [termMode]);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const b64data = util.stringToBase64(asciiVal);
|
|
||||||
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: blockId, inputdata64: b64data });
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
let termMode = blockData?.meta?.["term:mode"] ?? "term";
|
|
||||||
if (termMode != "term" && termMode != "html") {
|
|
||||||
termMode = "term";
|
|
||||||
}
|
|
||||||
|
|
||||||
// set intitial controller status, and then subscribe for updates
|
// set intitial controller status, and then subscribe for updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -382,7 +320,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
if (status == null) {
|
if (status == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shellProcStatusRef.current = status;
|
model.shellProcStatusRef.current = status;
|
||||||
if (status == "running") {
|
if (status == "running") {
|
||||||
termRef.current?.setIsRunning(true);
|
termRef.current?.setIsRunning(true);
|
||||||
} else {
|
} else {
|
||||||
@ -418,26 +356,9 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
||||||
<TermStickers config={stickerConfig} />
|
<TermStickers config={stickerConfig} />
|
||||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||||
<div
|
<div key="htmlElem" className="term-htmlelem">
|
||||||
key="htmlElem"
|
|
||||||
className="term-htmlelem"
|
|
||||||
onClick={() => {
|
|
||||||
if (htmlElemFocusRef.current != null) {
|
|
||||||
htmlElemFocusRef.current.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div key="htmlElemFocus" className="term-htmlelem-focus">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={""}
|
|
||||||
ref={htmlElemFocusRef}
|
|
||||||
onKeyDown={handleHtmlKeyDown}
|
|
||||||
onChange={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div key="htmlElemContent" className="term-htmlelem-content">
|
<div key="htmlElemContent" className="term-htmlelem-content">
|
||||||
<VDomView rootNode={testVDom} />
|
<VDomView blockId={blockId} nodeModel={model.nodeModel} viewRef={viewRef} model={model.vdomModel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
528
frontend/app/view/term/vdom-model.tsx
Normal file
528
frontend/app/view/term/vdom-model.tsx
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { globalStore, WOS } from "@/app/store/global";
|
||||||
|
import { makeORef } from "@/app/store/wos";
|
||||||
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
|
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||||
|
import { TermWshClient } from "@/app/view/term/term-wsh";
|
||||||
|
import { NodeModel } from "@/layout/index";
|
||||||
|
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||||
|
import debug from "debug";
|
||||||
|
import * as jotai from "jotai";
|
||||||
|
|
||||||
|
const dlog = debug("wave:vdom");
|
||||||
|
|
||||||
|
type AtomContainer = {
|
||||||
|
val: any;
|
||||||
|
beVal: any;
|
||||||
|
usedBy: Set<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefContainer = {
|
||||||
|
refFn: (elem: HTMLElement) => void;
|
||||||
|
vdomRef: VDomRef;
|
||||||
|
elem: HTMLElement;
|
||||||
|
updated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeVDomIdMap(vdom: VDomElem, idMap: Map<string, VDomElem>) {
|
||||||
|
if (vdom == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vdom.waveid != null) {
|
||||||
|
idMap.set(vdom.waveid, vdom);
|
||||||
|
}
|
||||||
|
if (vdom.children == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let child of vdom.children) {
|
||||||
|
makeVDomIdMap(child, idMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertEvent(e: React.SyntheticEvent, fromProp: string): any {
|
||||||
|
if (e == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (fromProp == "onClick") {
|
||||||
|
return { type: "click" };
|
||||||
|
}
|
||||||
|
if (fromProp == "onKeyDown") {
|
||||||
|
const waveKeyEvent = adaptFromReactOrNativeKeyEvent(e as React.KeyboardEvent);
|
||||||
|
return waveKeyEvent;
|
||||||
|
}
|
||||||
|
if (fromProp == "onFocus") {
|
||||||
|
return { type: "focus" };
|
||||||
|
}
|
||||||
|
if (fromProp == "onBlur") {
|
||||||
|
return { type: "blur" };
|
||||||
|
}
|
||||||
|
return { type: "unknown" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VDomModel {
|
||||||
|
blockId: string;
|
||||||
|
nodeModel: NodeModel;
|
||||||
|
viewRef: React.RefObject<HTMLDivElement>;
|
||||||
|
vdomRoot: jotai.PrimitiveAtom<VDomElem> = jotai.atom();
|
||||||
|
atoms: Map<string, AtomContainer> = new Map(); // key is atomname
|
||||||
|
refs: Map<string, RefContainer> = new Map(); // key is refid
|
||||||
|
batchedEvents: VDomEvent[] = [];
|
||||||
|
messages: VDomMessage[] = [];
|
||||||
|
needsInitialization: boolean = true;
|
||||||
|
needsResync: boolean = true;
|
||||||
|
vdomNodeVersion: WeakMap<VDomElem, jotai.PrimitiveAtom<number>> = new WeakMap();
|
||||||
|
compoundAtoms: Map<string, jotai.PrimitiveAtom<{ [key: string]: any }>> = new Map();
|
||||||
|
rootRefId: string = crypto.randomUUID();
|
||||||
|
termWshClient: TermWshClient;
|
||||||
|
backendRoute: string;
|
||||||
|
backendOpts: VDomBackendOpts;
|
||||||
|
shouldDispose: boolean;
|
||||||
|
disposed: boolean;
|
||||||
|
hasPendingRequest: boolean;
|
||||||
|
needsUpdate: boolean;
|
||||||
|
maxNormalUpdateIntervalMs: number = 100;
|
||||||
|
needsImmediateUpdate: boolean;
|
||||||
|
lastUpdateTs: number = 0;
|
||||||
|
queuedUpdate: { timeoutId: any; ts: number; quick: boolean };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
blockId: string,
|
||||||
|
nodeModel: NodeModel,
|
||||||
|
viewRef: React.RefObject<HTMLDivElement>,
|
||||||
|
termWshClient: TermWshClient
|
||||||
|
) {
|
||||||
|
this.blockId = blockId;
|
||||||
|
this.nodeModel = nodeModel;
|
||||||
|
this.viewRef = viewRef;
|
||||||
|
this.termWshClient = termWshClient;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
globalStore.set(this.vdomRoot, null);
|
||||||
|
this.atoms.clear();
|
||||||
|
this.refs.clear();
|
||||||
|
this.batchedEvents = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.needsResync = true;
|
||||||
|
this.needsInitialization = true;
|
||||||
|
this.vdomNodeVersion = new WeakMap();
|
||||||
|
this.compoundAtoms.clear();
|
||||||
|
this.rootRefId = crypto.randomUUID();
|
||||||
|
this.backendRoute = null;
|
||||||
|
this.backendOpts = {};
|
||||||
|
this.shouldDispose = false;
|
||||||
|
this.disposed = false;
|
||||||
|
this.hasPendingRequest = false;
|
||||||
|
this.needsUpdate = false;
|
||||||
|
this.maxNormalUpdateIntervalMs = 100;
|
||||||
|
this.needsImmediateUpdate = false;
|
||||||
|
this.lastUpdateTs = 0;
|
||||||
|
this.queuedUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalKeydownHandler(e: WaveKeyboardEvent): boolean {
|
||||||
|
if (this.backendOpts?.closeonctrlc && checkKeyPressed(e, "Ctrl:c")) {
|
||||||
|
this.shouldDispose = true;
|
||||||
|
this.queueUpdate(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.backendOpts?.globalkeyboardevents) {
|
||||||
|
if (e.cmd || e.meta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.batchedEvents.push({
|
||||||
|
waveid: null,
|
||||||
|
propname: "onKeyDown",
|
||||||
|
eventdata: e,
|
||||||
|
});
|
||||||
|
this.queueUpdate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRefUpdates() {
|
||||||
|
for (let ref of this.refs.values()) {
|
||||||
|
if (ref.updated) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRefUpdates(): VDomRefUpdate[] {
|
||||||
|
let updates: VDomRefUpdate[] = [];
|
||||||
|
for (let ref of this.refs.values()) {
|
||||||
|
if (ref.updated || (ref.vdomRef.trackposition && ref.elem != null)) {
|
||||||
|
const ru: VDomRefUpdate = {
|
||||||
|
refid: ref.vdomRef.refid,
|
||||||
|
hascurrent: ref.vdomRef.hascurrent,
|
||||||
|
};
|
||||||
|
if (ref.vdomRef.trackposition && ref.elem != null) {
|
||||||
|
ru.position = {
|
||||||
|
offsetheight: ref.elem.offsetHeight,
|
||||||
|
offsetwidth: ref.elem.offsetWidth,
|
||||||
|
scrollheight: ref.elem.scrollHeight,
|
||||||
|
scrollwidth: ref.elem.scrollWidth,
|
||||||
|
scrolltop: ref.elem.scrollTop,
|
||||||
|
boundingclientrect: ref.elem.getBoundingClientRect(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
updates.push(ru);
|
||||||
|
ref.updated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueUpdate(quick: boolean = false, delay: number = 10) {
|
||||||
|
this.needsUpdate = true;
|
||||||
|
let nowTs = Date.now();
|
||||||
|
if (delay > this.maxNormalUpdateIntervalMs) {
|
||||||
|
delay = this.maxNormalUpdateIntervalMs;
|
||||||
|
}
|
||||||
|
if (quick) {
|
||||||
|
if (this.queuedUpdate) {
|
||||||
|
if (this.queuedUpdate.quick || this.queuedUpdate.ts <= nowTs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(this.queuedUpdate.timeoutId);
|
||||||
|
this.queuedUpdate = null;
|
||||||
|
}
|
||||||
|
let timeoutId = setTimeout(() => {
|
||||||
|
this._sendRenderRequest(true);
|
||||||
|
}, 0);
|
||||||
|
this.queuedUpdate = { timeoutId: timeoutId, ts: nowTs, quick: true };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.queuedUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let lastUpdateDiff = nowTs - this.lastUpdateTs;
|
||||||
|
let timeoutMs: number = null;
|
||||||
|
if (lastUpdateDiff >= this.maxNormalUpdateIntervalMs) {
|
||||||
|
// it has been a while since the last update, so use delay
|
||||||
|
timeoutMs = delay;
|
||||||
|
} else {
|
||||||
|
timeoutMs = this.maxNormalUpdateIntervalMs - lastUpdateDiff;
|
||||||
|
}
|
||||||
|
if (timeoutMs < delay) {
|
||||||
|
timeoutMs = delay;
|
||||||
|
}
|
||||||
|
let timeoutId = setTimeout(() => {
|
||||||
|
this._sendRenderRequest(false);
|
||||||
|
}, timeoutMs);
|
||||||
|
this.queuedUpdate = { timeoutId: timeoutId, ts: nowTs + timeoutMs, quick: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _sendRenderRequest(force: boolean) {
|
||||||
|
this.queuedUpdate = null;
|
||||||
|
if (this.disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.hasPendingRequest) {
|
||||||
|
if (force) {
|
||||||
|
this.needsImmediateUpdate = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!force && !this.needsUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.backendRoute == null) {
|
||||||
|
console.log("vdom-model", "no backend route");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hasPendingRequest = true;
|
||||||
|
this.needsImmediateUpdate = false;
|
||||||
|
try {
|
||||||
|
const feUpdate = this.createFeUpdate();
|
||||||
|
dlog("fe-update", feUpdate);
|
||||||
|
const beUpdate = await RpcApi.VDomRenderCommand(TabRpcClient, feUpdate, { route: this.backendRoute });
|
||||||
|
this.handleBackendUpdate(beUpdate);
|
||||||
|
} finally {
|
||||||
|
this.lastUpdateTs = Date.now();
|
||||||
|
this.hasPendingRequest = false;
|
||||||
|
}
|
||||||
|
if (this.needsImmediateUpdate) {
|
||||||
|
this.queueUpdate(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAtomContainer(atomName: string): AtomContainer {
|
||||||
|
let container = this.atoms.get(atomName);
|
||||||
|
if (container == null) {
|
||||||
|
container = {
|
||||||
|
val: null,
|
||||||
|
beVal: null,
|
||||||
|
usedBy: new Set(),
|
||||||
|
};
|
||||||
|
this.atoms.set(atomName, container);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreateRefContainer(vdomRef: VDomRef): RefContainer {
|
||||||
|
let container = this.refs.get(vdomRef.refid);
|
||||||
|
if (container == null) {
|
||||||
|
container = {
|
||||||
|
refFn: (elem: HTMLElement) => {
|
||||||
|
container.elem = elem;
|
||||||
|
const hasElem = elem != null;
|
||||||
|
if (vdomRef.hascurrent != hasElem) {
|
||||||
|
container.updated = true;
|
||||||
|
vdomRef.hascurrent = hasElem;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vdomRef: vdomRef,
|
||||||
|
elem: null,
|
||||||
|
updated: false,
|
||||||
|
};
|
||||||
|
this.refs.set(vdomRef.refid, container);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
tagUseAtoms(waveId: string, atomNames: Set<string>) {
|
||||||
|
for (let atomName of atomNames) {
|
||||||
|
let container = this.getAtomContainer(atomName);
|
||||||
|
container.usedBy.add(waveId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tagUnuseAtoms(waveId: string, atomNames: Set<string>) {
|
||||||
|
for (let atomName of atomNames) {
|
||||||
|
let container = this.getAtomContainer(atomName);
|
||||||
|
container.usedBy.delete(waveId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVDomNodeVersionAtom(vdom: VDomElem) {
|
||||||
|
let atom = this.vdomNodeVersion.get(vdom);
|
||||||
|
if (atom == null) {
|
||||||
|
atom = jotai.atom(0);
|
||||||
|
this.vdomNodeVersion.set(vdom, atom);
|
||||||
|
}
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
incVDomNodeVersion(vdom: VDomElem) {
|
||||||
|
if (vdom == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const atom = this.getVDomNodeVersionAtom(vdom);
|
||||||
|
globalStore.set(atom, globalStore.get(atom) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
addErrorMessage(message: string) {
|
||||||
|
this.messages.push({
|
||||||
|
messagetype: "error",
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenderUpdates(update: VDomBackendUpdate, idMap: Map<string, VDomElem>) {
|
||||||
|
if (!update.renderupdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let renderUpdate of update.renderupdates) {
|
||||||
|
if (renderUpdate.updatetype == "root") {
|
||||||
|
globalStore.set(this.vdomRoot, renderUpdate.vdom);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (renderUpdate.updatetype == "append") {
|
||||||
|
let parent = idMap.get(renderUpdate.waveid);
|
||||||
|
if (parent == null) {
|
||||||
|
this.addErrorMessage(`Could not find vdom with id ${renderUpdate.waveid} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parent.children == null) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
parent.children.push(renderUpdate.vdom);
|
||||||
|
this.incVDomNodeVersion(parent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (renderUpdate.updatetype == "replace") {
|
||||||
|
let parent = idMap.get(renderUpdate.waveid);
|
||||||
|
if (parent == null) {
|
||||||
|
this.addErrorMessage(`Could not find vdom with id ${renderUpdate.waveid} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (renderUpdate.index < 0 || parent.children == null || parent.children.length <= renderUpdate.index) {
|
||||||
|
this.addErrorMessage(`Could not find child at index ${renderUpdate.index} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parent.children[renderUpdate.index] = renderUpdate.vdom;
|
||||||
|
this.incVDomNodeVersion(parent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (renderUpdate.updatetype == "remove") {
|
||||||
|
let parent = idMap.get(renderUpdate.waveid);
|
||||||
|
if (parent == null) {
|
||||||
|
this.addErrorMessage(`Could not find vdom with id ${renderUpdate.waveid} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (renderUpdate.index < 0 || parent.children == null || parent.children.length <= renderUpdate.index) {
|
||||||
|
this.addErrorMessage(`Could not find child at index ${renderUpdate.index} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parent.children.splice(renderUpdate.index, 1);
|
||||||
|
this.incVDomNodeVersion(parent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (renderUpdate.updatetype == "insert") {
|
||||||
|
let parent = idMap.get(renderUpdate.waveid);
|
||||||
|
if (parent == null) {
|
||||||
|
this.addErrorMessage(`Could not find vdom with id ${renderUpdate.waveid} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (parent.children == null) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
if (renderUpdate.index < 0 || parent.children.length < renderUpdate.index) {
|
||||||
|
this.addErrorMessage(`Could not find child at index ${renderUpdate.index} (for renderupdates)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parent.children.splice(renderUpdate.index, 0, renderUpdate.vdom);
|
||||||
|
this.incVDomNodeVersion(parent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.addErrorMessage(`Unknown updatetype ${renderUpdate.updatetype}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAtomValue(atomName: string, value: any, fromBe: boolean, idMap: Map<string, VDomElem>) {
|
||||||
|
dlog("setAtomValue", atomName, value, fromBe);
|
||||||
|
let container = this.getAtomContainer(atomName);
|
||||||
|
container.val = value;
|
||||||
|
if (fromBe) {
|
||||||
|
container.beVal = value;
|
||||||
|
}
|
||||||
|
for (let id of container.usedBy) {
|
||||||
|
this.incVDomNodeVersion(idMap.get(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStateSync(update: VDomBackendUpdate, idMap: Map<string, VDomElem>) {
|
||||||
|
if (update.statesync == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let sync of update.statesync) {
|
||||||
|
this.setAtomValue(sync.atom, sync.value, true, idMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRefElem(refId: string): HTMLElement {
|
||||||
|
if (refId == this.rootRefId) {
|
||||||
|
return this.viewRef.current;
|
||||||
|
}
|
||||||
|
const ref = this.refs.get(refId);
|
||||||
|
return ref?.elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRefOperations(update: VDomBackendUpdate, idMap: Map<string, VDomElem>) {
|
||||||
|
if (update.refoperations == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let refOp of update.refoperations) {
|
||||||
|
const elem = this.getRefElem(refOp.refid);
|
||||||
|
if (elem == null) {
|
||||||
|
this.addErrorMessage(`Could not find ref with id ${refOp.refid}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (refOp.op == "focus") {
|
||||||
|
if (elem == null) {
|
||||||
|
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: elem is null`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
elem.focus();
|
||||||
|
} catch (e) {
|
||||||
|
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: ${e.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addErrorMessage(`Unknown ref operation ${refOp.refid} ${refOp.op}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBackendUpdate(update: VDomBackendUpdate) {
|
||||||
|
if (update == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idMap = new Map<string, VDomElem>();
|
||||||
|
const vdomRoot = globalStore.get(this.vdomRoot);
|
||||||
|
if (update.opts != null) {
|
||||||
|
this.backendOpts = update.opts;
|
||||||
|
}
|
||||||
|
makeVDomIdMap(vdomRoot, idMap);
|
||||||
|
this.handleRenderUpdates(update, idMap);
|
||||||
|
this.handleStateSync(update, idMap);
|
||||||
|
this.handleRefOperations(update, idMap);
|
||||||
|
if (update.messages) {
|
||||||
|
for (let message of update.messages) {
|
||||||
|
console.log("vdom-message", this.blockId, message.messagetype, message.message);
|
||||||
|
if (message.stacktrace) {
|
||||||
|
console.log("vdom-message-stacktrace", message.stacktrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callVDomFunc(fnDecl: VDomFunc, e: any, compId: string, propName: string) {
|
||||||
|
const eventData = convertEvent(e, propName);
|
||||||
|
if (fnDecl.globalevent) {
|
||||||
|
const waveEvent: VDomEvent = {
|
||||||
|
waveid: null,
|
||||||
|
propname: fnDecl.globalevent,
|
||||||
|
eventdata: eventData,
|
||||||
|
};
|
||||||
|
this.batchedEvents.push(waveEvent);
|
||||||
|
} else {
|
||||||
|
const vdomEvent: VDomEvent = {
|
||||||
|
waveid: compId,
|
||||||
|
propname: propName,
|
||||||
|
eventdata: eventData,
|
||||||
|
};
|
||||||
|
this.batchedEvents.push(vdomEvent);
|
||||||
|
}
|
||||||
|
this.queueUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
createFeUpdate(): VDomFrontendUpdate {
|
||||||
|
const blockORef = makeORef("block", this.blockId);
|
||||||
|
const blockAtom = WOS.getWaveObjectAtom<Block>(blockORef);
|
||||||
|
const blockData = globalStore.get(blockAtom);
|
||||||
|
const isBlockFocused = globalStore.get(this.nodeModel.isFocused);
|
||||||
|
const renderContext: VDomRenderContext = {
|
||||||
|
blockid: this.blockId,
|
||||||
|
focused: isBlockFocused,
|
||||||
|
width: this.viewRef?.current?.offsetWidth ?? 0,
|
||||||
|
height: this.viewRef?.current?.offsetHeight ?? 0,
|
||||||
|
rootrefid: this.rootRefId,
|
||||||
|
background: false,
|
||||||
|
};
|
||||||
|
const feUpdate: VDomFrontendUpdate = {
|
||||||
|
type: "frontendupdate",
|
||||||
|
ts: Date.now(),
|
||||||
|
blockid: this.blockId,
|
||||||
|
initialize: this.needsInitialization,
|
||||||
|
rendercontext: renderContext,
|
||||||
|
dispose: this.shouldDispose,
|
||||||
|
resync: this.needsResync,
|
||||||
|
events: this.batchedEvents,
|
||||||
|
refupdates: this.getRefUpdates(),
|
||||||
|
};
|
||||||
|
this.needsResync = false;
|
||||||
|
this.needsInitialization = false;
|
||||||
|
this.batchedEvents = [];
|
||||||
|
if (this.shouldDispose) {
|
||||||
|
this.disposed = true;
|
||||||
|
}
|
||||||
|
return feUpdate;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,25 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||||
|
import { NodeModel } from "@/layout/index";
|
||||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||||
|
import { useAtomValueSafe } from "@/util/util";
|
||||||
|
import debug from "debug";
|
||||||
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
const TextTag = "#text";
|
||||||
|
const FragmentTag = "#fragment";
|
||||||
|
const WaveTextTag = "wave:text";
|
||||||
|
const WaveNullTag = "wave:null";
|
||||||
|
|
||||||
|
const VDomObjType_Ref = "ref";
|
||||||
|
const VDomObjType_Binding = "binding";
|
||||||
|
const VDomObjType_Func = "func";
|
||||||
|
|
||||||
|
const dlog = debug("wave:vdom");
|
||||||
|
|
||||||
const AllowedTags: { [tagName: string]: boolean } = {
|
const AllowedTags: { [tagName: string]: boolean } = {
|
||||||
div: true,
|
div: true,
|
||||||
b: true,
|
b: true,
|
||||||
@ -30,38 +46,38 @@ const AllowedTags: { [tagName: string]: boolean } = {
|
|||||||
form: true,
|
form: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function convertVDomFunc(fnDecl: VDomFuncType, compId: string, propName: string): (e: any) => void {
|
function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void {
|
||||||
return (e: any) => {
|
return (e: any) => {
|
||||||
if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) {
|
if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) {
|
||||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
for (let keyDesc of fnDecl["#keys"]) {
|
for (let keyDesc of fnDecl.keys || []) {
|
||||||
if (checkKeyPressed(waveEvent, keyDesc)) {
|
if (checkKeyPressed(waveEvent, keyDesc)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
callFunc(e, compId, propName);
|
model.callVDomFunc(fnDecl, e, compId, propName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fnDecl["#preventDefault"]) {
|
if (fnDecl.preventdefault) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (fnDecl["#stopPropagation"]) {
|
if (fnDecl.stoppropagation) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
callFunc(e, compId, propName);
|
model.callVDomFunc(fnDecl, e, compId, propName);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertElemToTag(elem: VDomElem): JSX.Element | string {
|
function convertElemToTag(elem: VDomElem, model: VDomModel): JSX.Element | string {
|
||||||
if (elem == null) {
|
if (elem == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (elem.tag == "#text") {
|
if (elem.tag == TextTag) {
|
||||||
return elem.text;
|
return elem.text;
|
||||||
}
|
}
|
||||||
return React.createElement(VDomTag, { elem: elem, key: elem.id });
|
return React.createElement(VDomTag, { key: elem.waveid, elem, model });
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObject(v: any): boolean {
|
function isObject(v: any): boolean {
|
||||||
@ -72,19 +88,35 @@ function isArray(v: any): boolean {
|
|||||||
return Array.isArray(v);
|
return Array.isArray(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function callFunc(e: any, compId: string, propName: string) {
|
function resolveBinding(binding: VDomBinding, model: VDomModel): [any, string[]] {
|
||||||
console.log("callfunc", compId, propName);
|
const bindName = binding.bind;
|
||||||
}
|
if (bindName == null || bindName == "") {
|
||||||
|
return [null, []];
|
||||||
function updateRefFunc(elem: any, ref: VDomRefType) {
|
}
|
||||||
console.log("updateref", ref["#ref"], elem);
|
// for now we only recognize $.[atomname] bindings
|
||||||
}
|
if (!bindName.startsWith("$.")) {
|
||||||
|
return [null, []];
|
||||||
function VDomTag({ elem }: { elem: VDomElem }) {
|
}
|
||||||
if (!AllowedTags[elem.tag]) {
|
const atomName = bindName.substring(2);
|
||||||
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
|
if (atomName == "") {
|
||||||
|
return [null, []];
|
||||||
|
}
|
||||||
|
const atom = model.getAtomContainer(atomName);
|
||||||
|
if (atom == null) {
|
||||||
|
return [null, []];
|
||||||
|
}
|
||||||
|
return [atom.val, [atomName]];
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenericPropsType = { [key: string]: any };
|
||||||
|
|
||||||
|
// returns props, and a set of atom keys used in the props
|
||||||
|
function convertProps(elem: VDomElem, model: VDomModel): [GenericPropsType, Set<string>] {
|
||||||
|
let props: GenericPropsType = {};
|
||||||
|
let atomKeys = new Set<string>();
|
||||||
|
if (elem.props == null) {
|
||||||
|
return [props, atomKeys];
|
||||||
}
|
}
|
||||||
let props = {};
|
|
||||||
for (let key in elem.props) {
|
for (let key in elem.props) {
|
||||||
let val = elem.props[key];
|
let val = elem.props[key];
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
@ -94,35 +126,149 @@ function VDomTag({ elem }: { elem: VDomElem }) {
|
|||||||
if (val == null) {
|
if (val == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isObject(val) && "#ref" in val) {
|
if (isObject(val) && val.type == VDomObjType_Ref) {
|
||||||
props[key] = (elem: HTMLElement) => {
|
const valRef = val as VDomRef;
|
||||||
updateRefFunc(elem, val);
|
const refContainer = model.getOrCreateRefContainer(valRef);
|
||||||
};
|
props[key] = refContainer.refFn;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isObject(val) && "#func" in val) {
|
if (isObject(val) && val.type == VDomObjType_Func) {
|
||||||
props[key] = convertVDomFunc(val, elem.id, key);
|
const valFunc = val as VDomFunc;
|
||||||
|
props[key] = convertVDomFunc(model, valFunc, elem.waveid, key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isObject(val) && val.type == VDomObjType_Binding) {
|
||||||
|
const [propVal, atomDeps] = resolveBinding(val as VDomBinding, model);
|
||||||
|
props[key] = propVal;
|
||||||
|
for (let atomDep of atomDeps) {
|
||||||
|
atomKeys.add(atomDep);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key == "style" && isObject(val)) {
|
||||||
|
// assuming the entire style prop wasn't bound, look through the individual keys and bind them
|
||||||
|
for (let styleKey in val) {
|
||||||
|
let styleVal = val[styleKey];
|
||||||
|
if (isObject(styleVal) && styleVal.type == VDomObjType_Binding) {
|
||||||
|
const [stylePropVal, styleAtomDeps] = resolveBinding(styleVal as VDomBinding, model);
|
||||||
|
val[styleKey] = stylePropVal;
|
||||||
|
for (let styleAtomDep of styleAtomDeps) {
|
||||||
|
atomKeys.add(styleAtomDep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallthrough to set props[key] = val
|
||||||
|
}
|
||||||
|
props[key] = val;
|
||||||
}
|
}
|
||||||
|
return [props, atomKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertChildren(elem: VDomElem, model: VDomModel): (string | JSX.Element)[] {
|
||||||
let childrenComps: (string | JSX.Element)[] = [];
|
let childrenComps: (string | JSX.Element)[] = [];
|
||||||
if (elem.children) {
|
if (elem.children == null) {
|
||||||
for (let child of elem.children) {
|
|
||||||
if (child == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
childrenComps.push(convertElemToTag(child));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (elem.tag == "#fragment") {
|
|
||||||
return childrenComps;
|
return childrenComps;
|
||||||
}
|
}
|
||||||
|
for (let child of elem.children) {
|
||||||
|
if (child == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
childrenComps.push(convertElemToTag(child, model));
|
||||||
|
}
|
||||||
|
return childrenComps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringSetsEqual(set1: Set<string>, set2: Set<string>): boolean {
|
||||||
|
if (set1.size != set2.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let elem of set1) {
|
||||||
|
if (!set2.has(elem)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||||
|
const version = jotai.useAtomValue(model.getVDomNodeVersionAtom(elem));
|
||||||
|
const [oldAtomKeys, setOldAtomKeys] = React.useState<Set<string>>(new Set());
|
||||||
|
let [props, atomKeys] = convertProps(elem, model);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (stringSetsEqual(atomKeys, oldAtomKeys)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
model.tagUnuseAtoms(elem.waveid, oldAtomKeys);
|
||||||
|
model.tagUseAtoms(elem.waveid, atomKeys);
|
||||||
|
setOldAtomKeys(atomKeys);
|
||||||
|
}, [atomKeys]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
model.tagUnuseAtoms(elem.waveid, oldAtomKeys);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (elem.tag == WaveNullTag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (elem.tag == WaveTextTag) {
|
||||||
|
return props.text;
|
||||||
|
}
|
||||||
|
if (!AllowedTags[elem.tag]) {
|
||||||
|
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
|
||||||
|
}
|
||||||
|
let childrenComps = convertChildren(elem, model);
|
||||||
|
dlog("children", childrenComps);
|
||||||
|
if (elem.tag == FragmentTag) {
|
||||||
|
return childrenComps;
|
||||||
|
}
|
||||||
|
props.key = "e-" + elem.waveid;
|
||||||
return React.createElement(elem.tag, props, childrenComps);
|
return React.createElement(elem.tag, props, childrenComps);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VDomView({ rootNode }: { rootNode: VDomElem }) {
|
function vdomText(text: string): VDomElem {
|
||||||
let rtn = convertElemToTag(rootNode);
|
return {
|
||||||
|
tag: "#text",
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const testVDom: VDomElem = {
|
||||||
|
waveid: "testid1",
|
||||||
|
tag: "div",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
waveid: "testh1",
|
||||||
|
tag: "h1",
|
||||||
|
children: [vdomText("Hello World")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
waveid: "testp",
|
||||||
|
tag: "p",
|
||||||
|
children: [vdomText("This is a paragraph (from VDOM)")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function VDomView({
|
||||||
|
blockId,
|
||||||
|
nodeModel,
|
||||||
|
viewRef,
|
||||||
|
model,
|
||||||
|
}: {
|
||||||
|
blockId: string;
|
||||||
|
nodeModel: NodeModel;
|
||||||
|
viewRef: React.RefObject<HTMLDivElement>;
|
||||||
|
model: VDomModel;
|
||||||
|
}) {
|
||||||
|
let rootNode = useAtomValueSafe(model?.vdomRoot);
|
||||||
|
if (!model || viewRef.current == null || rootNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
dlog("render", rootNode);
|
||||||
|
model.viewRef = viewRef;
|
||||||
|
let rtn = convertElemToTag(rootNode, model);
|
||||||
return <div className="vdom">{rtn}</div>;
|
return <div className="vdom">{rtn}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -274,6 +274,7 @@ declare global {
|
|||||||
getSettingsMenuItems?: () => ContextMenuItem[];
|
getSettingsMenuItems?: () => ContextMenuItem[];
|
||||||
giveFocus?: () => boolean;
|
giveFocus?: () => boolean;
|
||||||
keyDownHandler?: (e: WaveKeyboardEvent) => boolean;
|
keyDownHandler?: (e: WaveKeyboardEvent) => boolean;
|
||||||
|
dispose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdaterStatus = "up-to-date" | "checking" | "downloading" | "ready" | "error" | "installing";
|
type UpdaterStatus = "up-to-date" | "checking" | "downloading" | "ready" | "error" | "installing";
|
||||||
|
160
frontend/types/gotypes.d.ts
vendored
160
frontend/types/gotypes.d.ts
vendored
@ -192,6 +192,16 @@ declare global {
|
|||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// vdom.DomRect
|
||||||
|
type DomRect = {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
// waveobj.FileDef
|
// waveobj.FileDef
|
||||||
type FileDef = {
|
type FileDef = {
|
||||||
filetype?: string;
|
filetype?: string;
|
||||||
@ -324,6 +334,9 @@ declare global {
|
|||||||
"term:localshellpath"?: string;
|
"term:localshellpath"?: string;
|
||||||
"term:localshellopts"?: string[];
|
"term:localshellopts"?: string[];
|
||||||
"term:scrollback"?: number;
|
"term:scrollback"?: number;
|
||||||
|
"vdom:*"?: boolean;
|
||||||
|
"vdom:initialized"?: boolean;
|
||||||
|
"vdom:correlationid"?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -588,27 +601,150 @@ declare global {
|
|||||||
checkboxstat?: boolean;
|
checkboxstat?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// vdom.Elem
|
// vdom.VDomAsyncInitiationRequest
|
||||||
|
type VDomAsyncInitiationRequest = {
|
||||||
|
type: "asyncinitiationrequest";
|
||||||
|
ts: number;
|
||||||
|
blockid?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomBackendOpts
|
||||||
|
type VDomBackendOpts = {
|
||||||
|
closeonctrlc?: boolean;
|
||||||
|
globalkeyboardevents?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomBackendUpdate
|
||||||
|
type VDomBackendUpdate = {
|
||||||
|
type: "backendupdate";
|
||||||
|
ts: number;
|
||||||
|
blockid: string;
|
||||||
|
opts?: VDomBackendOpts;
|
||||||
|
renderupdates?: VDomRenderUpdate[];
|
||||||
|
statesync?: VDomStateSync[];
|
||||||
|
refoperations?: VDomRefOperation[];
|
||||||
|
messages?: VDomMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomBinding
|
||||||
|
type VDomBinding = {
|
||||||
|
type: "binding";
|
||||||
|
bind: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomCreateContext
|
||||||
|
type VDomCreateContext = {
|
||||||
|
type: "createcontext";
|
||||||
|
ts: number;
|
||||||
|
meta?: MetaType;
|
||||||
|
newblock?: boolean;
|
||||||
|
persist?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomElem
|
||||||
type VDomElem = {
|
type VDomElem = {
|
||||||
id?: string;
|
waveid?: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
props?: {[key: string]: any};
|
props?: {[key: string]: any};
|
||||||
children?: VDomElem[];
|
children?: VDomElem[];
|
||||||
text?: string;
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// vdom.VDomFuncType
|
// vdom.VDomEvent
|
||||||
type VDomFuncType = {
|
type VDomEvent = {
|
||||||
#func: string;
|
waveid: string;
|
||||||
#stopPropagation?: boolean;
|
propname: string;
|
||||||
#preventDefault?: boolean;
|
eventdata: any;
|
||||||
#keys?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// vdom.VDomRefType
|
// vdom.VDomFrontendUpdate
|
||||||
type VDomRefType = {
|
type VDomFrontendUpdate = {
|
||||||
#ref: string;
|
type: "frontendupdate";
|
||||||
current: any;
|
ts: number;
|
||||||
|
blockid: string;
|
||||||
|
correlationid?: string;
|
||||||
|
initialize?: boolean;
|
||||||
|
dispose?: boolean;
|
||||||
|
resync?: boolean;
|
||||||
|
rendercontext?: VDomRenderContext;
|
||||||
|
events?: VDomEvent[];
|
||||||
|
statesync?: VDomStateSync[];
|
||||||
|
refupdates?: VDomRefUpdate[];
|
||||||
|
messages?: VDomMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomFunc
|
||||||
|
type VDomFunc = {
|
||||||
|
type: "func";
|
||||||
|
stoppropagation?: boolean;
|
||||||
|
preventdefault?: boolean;
|
||||||
|
globalevent?: string;
|
||||||
|
keys?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomMessage
|
||||||
|
type VDomMessage = {
|
||||||
|
messagetype: string;
|
||||||
|
message: string;
|
||||||
|
stacktrace?: string;
|
||||||
|
params?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRef
|
||||||
|
type VDomRef = {
|
||||||
|
type: "ref";
|
||||||
|
refid: string;
|
||||||
|
trackposition?: boolean;
|
||||||
|
position?: VDomRefPosition;
|
||||||
|
hascurrent?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRefOperation
|
||||||
|
type VDomRefOperation = {
|
||||||
|
refid: string;
|
||||||
|
op: string;
|
||||||
|
params?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRefPosition
|
||||||
|
type VDomRefPosition = {
|
||||||
|
offsetheight: number;
|
||||||
|
offsetwidth: number;
|
||||||
|
scrollheight: number;
|
||||||
|
scrollwidth: number;
|
||||||
|
scrolltop: number;
|
||||||
|
boundingclientrect: DomRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRefUpdate
|
||||||
|
type VDomRefUpdate = {
|
||||||
|
refid: string;
|
||||||
|
hascurrent: boolean;
|
||||||
|
position?: VDomRefPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRenderContext
|
||||||
|
type VDomRenderContext = {
|
||||||
|
blockid: string;
|
||||||
|
focused: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
rootrefid: string;
|
||||||
|
background?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRenderUpdate
|
||||||
|
type VDomRenderUpdate = {
|
||||||
|
updatetype: "root"|"append"|"replace"|"remove"|"insert";
|
||||||
|
waveid?: string;
|
||||||
|
vdom: VDomElem;
|
||||||
|
index?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomStateSync
|
||||||
|
type VDomStateSync = {
|
||||||
|
atom: string;
|
||||||
|
value: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WSCommandType = {
|
type WSCommandType = {
|
||||||
|
@ -241,6 +241,56 @@ function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent {
|
|||||||
return rtn;
|
return rtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyMap = {
|
||||||
|
Enter: "\r",
|
||||||
|
Backspace: "\x7f",
|
||||||
|
Tab: "\t",
|
||||||
|
Escape: "\x1b",
|
||||||
|
ArrowUp: "\x1b[A",
|
||||||
|
ArrowDown: "\x1b[B",
|
||||||
|
ArrowRight: "\x1b[C",
|
||||||
|
ArrowLeft: "\x1b[D",
|
||||||
|
Insert: "\x1b[2~",
|
||||||
|
Delete: "\x1b[3~",
|
||||||
|
Home: "\x1b[1~",
|
||||||
|
End: "\x1b[4~",
|
||||||
|
PageUp: "\x1b[5~",
|
||||||
|
PageDown: "\x1b[6~",
|
||||||
|
};
|
||||||
|
|
||||||
|
function keyboardEventToASCII(event: WaveKeyboardEvent): string {
|
||||||
|
// check modifiers
|
||||||
|
// if no modifiers are set, just send the key
|
||||||
|
if (!event.alt && !event.control && !event.meta) {
|
||||||
|
if (event.key == null || event.key == "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (keyMap[event.key] != null) {
|
||||||
|
return keyMap[event.key];
|
||||||
|
}
|
||||||
|
if (event.key.length == 1) {
|
||||||
|
return event.key;
|
||||||
|
} else {
|
||||||
|
console.log("not sending keyboard event", event.key, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if meta or alt is set, there is no ASCII representation
|
||||||
|
if (event.meta || event.alt) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// if ctrl is set, if it is a letter, subtract 64 from the uppercase value to get the ASCII value
|
||||||
|
if (event.control) {
|
||||||
|
if (
|
||||||
|
(event.key.length === 1 && event.key >= "A" && event.key <= "Z") ||
|
||||||
|
(event.key >= "a" && event.key <= "z")
|
||||||
|
) {
|
||||||
|
const key = event.key.toUpperCase();
|
||||||
|
return String.fromCharCode(key.charCodeAt(0) - 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
adaptFromElectronKeyEvent,
|
adaptFromElectronKeyEvent,
|
||||||
adaptFromReactOrNativeKeyEvent,
|
adaptFromReactOrNativeKeyEvent,
|
||||||
@ -248,6 +298,7 @@ export {
|
|||||||
getKeyUtilPlatform,
|
getKeyUtilPlatform,
|
||||||
isCharacterKeyEvent,
|
isCharacterKeyEvent,
|
||||||
isInputEvent,
|
isInputEvent,
|
||||||
|
keyboardEventToASCII,
|
||||||
keydownWrapper,
|
keydownWrapper,
|
||||||
parseKeyDescription,
|
parseKeyDescription,
|
||||||
setKeyUtilPlatform,
|
setKeyUtilPlatform,
|
||||||
|
@ -42,9 +42,13 @@ var ExtraTypes = []any{
|
|||||||
wshutil.RpcMessage{},
|
wshutil.RpcMessage{},
|
||||||
wshrpc.WshServerCommandMeta{},
|
wshrpc.WshServerCommandMeta{},
|
||||||
userinput.UserInputRequest{},
|
userinput.UserInputRequest{},
|
||||||
vdom.Elem{},
|
vdom.VDomCreateContext{},
|
||||||
vdom.VDomFuncType{},
|
vdom.VDomElem{},
|
||||||
vdom.VDomRefType{},
|
vdom.VDomFunc{},
|
||||||
|
vdom.VDomRef{},
|
||||||
|
vdom.VDomBinding{},
|
||||||
|
vdom.VDomFrontendUpdate{},
|
||||||
|
vdom.VDomBackendUpdate{},
|
||||||
waveobj.MetaTSType{},
|
waveobj.MetaTSType{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
89
pkg/util/utilfn/compare.go
Normal file
89
pkg/util/utilfn/compare.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package utilfn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// this is a shallow equal, but with special handling for numeric types
|
||||||
|
// it will up convert to float64 and compare
|
||||||
|
func JsonValEqual(a, b any) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
typeA := reflect.TypeOf(a)
|
||||||
|
typeB := reflect.TypeOf(b)
|
||||||
|
if typeA == typeB && typeA.Comparable() {
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
if IsNumericType(a) && IsNumericType(b) {
|
||||||
|
return CompareAsFloat64(a, b)
|
||||||
|
}
|
||||||
|
if typeA != typeB {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// for slices and maps, compare their pointers
|
||||||
|
valA := reflect.ValueOf(a)
|
||||||
|
valB := reflect.ValueOf(b)
|
||||||
|
switch valA.Kind() {
|
||||||
|
case reflect.Slice, reflect.Map:
|
||||||
|
return valA.Pointer() == valB.Pointer()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if a value is a numeric type
|
||||||
|
func IsNumericType(val any) bool {
|
||||||
|
switch val.(type) {
|
||||||
|
case int, int8, int16, int32, int64,
|
||||||
|
uint, uint8, uint16, uint32, uint64,
|
||||||
|
float32, float64:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to handle numeric comparisons as float64
|
||||||
|
func CompareAsFloat64(a, b any) bool {
|
||||||
|
valA, okA := ToFloat64(a)
|
||||||
|
valB, okB := ToFloat64(b)
|
||||||
|
return okA && okB && valA == valB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert various numeric types to float64 for comparison
|
||||||
|
func ToFloat64(val any) (float64, bool) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int:
|
||||||
|
return float64(v), true
|
||||||
|
case int8:
|
||||||
|
return float64(v), true
|
||||||
|
case int16:
|
||||||
|
return float64(v), true
|
||||||
|
case int32:
|
||||||
|
return float64(v), true
|
||||||
|
case int64:
|
||||||
|
return float64(v), true
|
||||||
|
case uint:
|
||||||
|
return float64(v), true
|
||||||
|
case uint8:
|
||||||
|
return float64(v), true
|
||||||
|
case uint16:
|
||||||
|
return float64(v), true
|
||||||
|
case uint32:
|
||||||
|
return float64(v), true
|
||||||
|
case uint64:
|
||||||
|
return float64(v), true
|
||||||
|
case float32:
|
||||||
|
return float64(v), true
|
||||||
|
case float64:
|
||||||
|
return v, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
159
pkg/vdom/cssparser/cssparser.go
Normal file
159
pkg/vdom/cssparser/cssparser.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cssparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
Input string
|
||||||
|
Pos int
|
||||||
|
Length int
|
||||||
|
InQuote bool
|
||||||
|
QuoteChar rune
|
||||||
|
OpenParens int
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeParser(input string) *Parser {
|
||||||
|
return &Parser{
|
||||||
|
Input: input,
|
||||||
|
Length: len(input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Parse() (map[string]string, error) {
|
||||||
|
result := make(map[string]string)
|
||||||
|
lastProp := ""
|
||||||
|
for {
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.eof() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
propName, err := p.parseIdentifierColon(lastProp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lastProp = propName
|
||||||
|
p.skipWhitespace()
|
||||||
|
value, err := p.parseValue(propName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[propName] = value
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.eof() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !p.expectChar(';') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.skipWhitespace()
|
||||||
|
if !p.eof() {
|
||||||
|
return nil, fmt.Errorf("bad style attribute, unexpected character %q at pos %d", string(p.Input[p.Pos]), p.Pos+1)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseIdentifierColon(lastProp string) (string, error) {
|
||||||
|
start := p.Pos
|
||||||
|
for !p.eof() {
|
||||||
|
c := p.peekChar()
|
||||||
|
if isIdentChar(c) || c == '-' {
|
||||||
|
p.advance()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attrName := p.Input[start:p.Pos]
|
||||||
|
p.skipWhitespace()
|
||||||
|
if p.eof() {
|
||||||
|
return "", fmt.Errorf("bad style attribute, expected colon after property %q, got EOF, at pos %d", attrName, p.Pos+1)
|
||||||
|
}
|
||||||
|
if attrName == "" {
|
||||||
|
return "", fmt.Errorf("bad style attribute, invalid property name after property %q, at pos %d", lastProp, p.Pos+1)
|
||||||
|
}
|
||||||
|
if !p.expectChar(':') {
|
||||||
|
return "", fmt.Errorf("bad style attribute, bad property name starting with %q, expected colon, got %q, at pos %d", attrName, string(p.Input[p.Pos]), p.Pos+1)
|
||||||
|
}
|
||||||
|
return attrName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseValue(propName string) (string, error) {
|
||||||
|
start := p.Pos
|
||||||
|
quotePos := 0
|
||||||
|
parenPosStack := make([]int, 0)
|
||||||
|
for !p.eof() {
|
||||||
|
c := p.peekChar()
|
||||||
|
if p.InQuote {
|
||||||
|
if c == p.QuoteChar {
|
||||||
|
p.InQuote = false
|
||||||
|
} else if c == '\\' {
|
||||||
|
p.advance()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c == '"' || c == '\'' {
|
||||||
|
p.InQuote = true
|
||||||
|
p.QuoteChar = c
|
||||||
|
quotePos = p.Pos
|
||||||
|
} else if c == '(' {
|
||||||
|
p.OpenParens++
|
||||||
|
parenPosStack = append(parenPosStack, p.Pos)
|
||||||
|
} else if c == ')' {
|
||||||
|
if p.OpenParens == 0 {
|
||||||
|
return "", fmt.Errorf("unmatched ')' at pos %d", p.Pos+1)
|
||||||
|
}
|
||||||
|
p.OpenParens--
|
||||||
|
parenPosStack = parenPosStack[:len(parenPosStack)-1]
|
||||||
|
} else if c == ';' && p.OpenParens == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.advance()
|
||||||
|
}
|
||||||
|
if p.eof() && p.InQuote {
|
||||||
|
return "", fmt.Errorf("bad style attribute, while parsing attribute %q, unmatched quote at pos %d", propName, quotePos+1)
|
||||||
|
}
|
||||||
|
if p.eof() && p.OpenParens > 0 {
|
||||||
|
return "", fmt.Errorf("bad style attribute, while parsing property %q, unmatched '(' at pos %d", propName, parenPosStack[len(parenPosStack)-1]+1)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(p.Input[start:p.Pos]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIdentChar(r rune) bool {
|
||||||
|
return unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) skipWhitespace() {
|
||||||
|
for !p.eof() && unicode.IsSpace(p.peekChar()) {
|
||||||
|
p.advance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) expectChar(expected rune) bool {
|
||||||
|
if !p.eof() && p.peekChar() == expected {
|
||||||
|
p.advance()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) peekChar() rune {
|
||||||
|
if p.Pos >= p.Length {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return rune(p.Input[p.Pos])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) advance() {
|
||||||
|
p.Pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) eof() bool {
|
||||||
|
return p.Pos >= p.Length
|
||||||
|
}
|
81
pkg/vdom/cssparser/cssparser_test.go
Normal file
81
pkg/vdom/cssparser/cssparser_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cssparser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func compareMaps(a, b map[string]string) error {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return fmt.Errorf("map length mismatch: %d != %d", len(a), len(b))
|
||||||
|
}
|
||||||
|
for k, v := range a {
|
||||||
|
if b[k] != v {
|
||||||
|
return fmt.Errorf("value mismatch for key %s: %q != %q", k, v, b[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse1(t *testing.T) {
|
||||||
|
style := `background: url("example;with;semicolons.jpg"); color: red; margin-right: 5px; content: "hello;world";`
|
||||||
|
p := MakeParser(style)
|
||||||
|
parsed, err := p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expected := map[string]string{
|
||||||
|
"background": `url("example;with;semicolons.jpg")`,
|
||||||
|
"color": "red",
|
||||||
|
"margin-right": "5px",
|
||||||
|
"content": `"hello;world"`,
|
||||||
|
}
|
||||||
|
if err := compareMaps(parsed, expected); err != nil {
|
||||||
|
t.Fatalf("Parsed map does not match expected: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
style = `margin-right: calc(10px + 5px); color: red; font-family: "Arial";`
|
||||||
|
p = MakeParser(style)
|
||||||
|
parsed, err = p.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expected = map[string]string{
|
||||||
|
"margin-right": `calc(10px + 5px)`,
|
||||||
|
"color": "red",
|
||||||
|
"font-family": `"Arial"`,
|
||||||
|
}
|
||||||
|
if err := compareMaps(parsed, expected); err != nil {
|
||||||
|
t.Fatalf("Parsed map does not match expected: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParserErrors(t *testing.T) {
|
||||||
|
style := `hello more: bad;`
|
||||||
|
p := MakeParser(style)
|
||||||
|
_, err := p.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
log.Printf("got expected error: %v\n", err)
|
||||||
|
style = `background: url("example.jpg`
|
||||||
|
p = MakeParser(style)
|
||||||
|
_, err = p.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
log.Printf("got expected error: %v\n", err)
|
||||||
|
style = `foo: url(...`
|
||||||
|
p = MakeParser(style)
|
||||||
|
_, err = p.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
log.Printf("got expected error: %v\n", err)
|
||||||
|
}
|
108
pkg/vdom/vdom.go
108
pkg/vdom/vdom.go
@ -15,35 +15,6 @@ import (
|
|||||||
|
|
||||||
// ReactNode types = nil | string | Elem
|
// ReactNode types = nil | string | Elem
|
||||||
|
|
||||||
const TextTag = "#text"
|
|
||||||
const FragmentTag = "#fragment"
|
|
||||||
|
|
||||||
const ChildrenPropKey = "children"
|
|
||||||
const KeyPropKey = "key"
|
|
||||||
|
|
||||||
// doubles as VDOM structure
|
|
||||||
type Elem struct {
|
|
||||||
Id string `json:"id,omitempty"` // used for vdom
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
Props map[string]any `json:"props,omitempty"`
|
|
||||||
Children []Elem `json:"children,omitempty"`
|
|
||||||
Text string `json:"text,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VDomRefType struct {
|
|
||||||
RefId string `json:"#ref"`
|
|
||||||
Current any `json:"current"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// can be used to set preventDefault/stopPropagation
|
|
||||||
type VDomFuncType struct {
|
|
||||||
Fn any `json:"-"` // the actual function to call (called via reflection)
|
|
||||||
FuncType string `json:"#func"`
|
|
||||||
StopPropagation bool `json:"#stopPropagation,omitempty"`
|
|
||||||
PreventDefault bool `json:"#preventDefault,omitempty"`
|
|
||||||
Keys []string `json:"#keys,omitempty"` // special for keyDown events a list of keys to "capture"
|
|
||||||
}
|
|
||||||
|
|
||||||
// generic hook structure
|
// generic hook structure
|
||||||
type Hook struct {
|
type Hook struct {
|
||||||
Init bool // is initialized
|
Init bool // is initialized
|
||||||
@ -56,7 +27,7 @@ type Hook struct {
|
|||||||
|
|
||||||
type CFunc = func(ctx context.Context, props map[string]any) any
|
type CFunc = func(ctx context.Context, props map[string]any) any
|
||||||
|
|
||||||
func (e *Elem) Key() string {
|
func (e *VDomElem) Key() string {
|
||||||
keyVal, ok := e.Props[KeyPropKey]
|
keyVal, ok := e.Props[KeyPropKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
@ -68,8 +39,8 @@ func (e *Elem) Key() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func TextElem(text string) Elem {
|
func TextElem(text string) VDomElem {
|
||||||
return Elem{Tag: TextTag, Text: text}
|
return VDomElem{Tag: TextTag, Text: text}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeProps(props *map[string]any, newProps map[string]any) {
|
func mergeProps(props *map[string]any, newProps map[string]any) {
|
||||||
@ -85,8 +56,8 @@ func mergeProps(props *map[string]any, newProps map[string]any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func E(tag string, parts ...any) *Elem {
|
func E(tag string, parts ...any) *VDomElem {
|
||||||
rtn := &Elem{Tag: tag}
|
rtn := &VDomElem{Tag: tag}
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if part == nil {
|
if part == nil {
|
||||||
continue
|
continue
|
||||||
@ -135,19 +106,44 @@ func UseState[T any](ctx context.Context, initialVal T) (T, func(T)) {
|
|||||||
}
|
}
|
||||||
setVal := func(newVal T) {
|
setVal := func(newVal T) {
|
||||||
hookVal.Val = newVal
|
hookVal.Val = newVal
|
||||||
vc.Root.AddRenderWork(vc.Comp.Id)
|
vc.Root.AddRenderWork(vc.Comp.WaveId)
|
||||||
}
|
}
|
||||||
return rtnVal, setVal
|
return rtnVal, setVal
|
||||||
}
|
}
|
||||||
|
|
||||||
func UseRef(ctx context.Context, initialVal any) *VDomRefType {
|
func UseAtom[T any](ctx context.Context, atomName string) (T, func(T)) {
|
||||||
vc, hookVal := getHookFromCtx(ctx)
|
vc, hookVal := getHookFromCtx(ctx)
|
||||||
if !hookVal.Init {
|
if !hookVal.Init {
|
||||||
hookVal.Init = true
|
hookVal.Init = true
|
||||||
refId := vc.Comp.Id + ":" + strconv.Itoa(hookVal.Idx)
|
closedWaveId := vc.Comp.WaveId
|
||||||
hookVal.Val = &VDomRefType{RefId: refId, Current: initialVal}
|
hookVal.UnmountFn = func() {
|
||||||
|
atom := vc.Root.GetAtom(atomName)
|
||||||
|
delete(atom.UsedBy, closedWaveId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
refVal, ok := hookVal.Val.(*VDomRefType)
|
atom := vc.Root.GetAtom(atomName)
|
||||||
|
atom.UsedBy[vc.Comp.WaveId] = true
|
||||||
|
atomVal, ok := atom.Val.(T)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("UseAtom %q value type mismatch (expected %T, got %T)", atomName, atomVal, atom.Val))
|
||||||
|
}
|
||||||
|
setVal := func(newVal T) {
|
||||||
|
atom.Val = newVal
|
||||||
|
for waveId := range atom.UsedBy {
|
||||||
|
vc.Root.AddRenderWork(waveId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return atomVal, setVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseVDomRef(ctx context.Context) *VDomRef {
|
||||||
|
vc, hookVal := getHookFromCtx(ctx)
|
||||||
|
if !hookVal.Init {
|
||||||
|
hookVal.Init = true
|
||||||
|
refId := vc.Comp.WaveId + ":" + strconv.Itoa(hookVal.Idx)
|
||||||
|
hookVal.Val = &VDomRef{Type: ObjectType_Ref, RefId: refId}
|
||||||
|
}
|
||||||
|
refVal, ok := hookVal.Val.(*VDomRef)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("UseRef hook value is not a ref (possible out of order or conditional hooks)")
|
panic("UseRef hook value is not a ref (possible out of order or conditional hooks)")
|
||||||
}
|
}
|
||||||
@ -159,7 +155,7 @@ func UseId(ctx context.Context) string {
|
|||||||
if vc == nil {
|
if vc == nil {
|
||||||
panic("UseId must be called within a component (no context)")
|
panic("UseId must be called within a component (no context)")
|
||||||
}
|
}
|
||||||
return vc.Comp.Id
|
return vc.Comp.WaveId
|
||||||
}
|
}
|
||||||
|
|
||||||
func depsEqual(deps1 []any, deps2 []any) bool {
|
func depsEqual(deps1 []any, deps2 []any) bool {
|
||||||
@ -181,7 +177,7 @@ func UseEffect(ctx context.Context, fn func() func(), deps []any) {
|
|||||||
hookVal.Init = true
|
hookVal.Init = true
|
||||||
hookVal.Fn = fn
|
hookVal.Fn = fn
|
||||||
hookVal.Deps = deps
|
hookVal.Deps = deps
|
||||||
vc.Root.AddEffectWork(vc.Comp.Id, hookVal.Idx)
|
vc.Root.AddEffectWork(vc.Comp.WaveId, hookVal.Idx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if depsEqual(hookVal.Deps, deps) {
|
if depsEqual(hookVal.Deps, deps) {
|
||||||
@ -189,7 +185,7 @@ func UseEffect(ctx context.Context, fn func() func(), deps []any) {
|
|||||||
}
|
}
|
||||||
hookVal.Fn = fn
|
hookVal.Fn = fn
|
||||||
hookVal.Deps = deps
|
hookVal.Deps = deps
|
||||||
vc.Root.AddEffectWork(vc.Comp.Id, hookVal.Idx)
|
vc.Root.AddEffectWork(vc.Comp.WaveId, hookVal.Idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func numToString[T any](value T) (string, bool) {
|
func numToString[T any](value T) (string, bool) {
|
||||||
@ -207,24 +203,24 @@ func numToString[T any](value T) (string, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func partToElems(part any) []Elem {
|
func partToElems(part any) []VDomElem {
|
||||||
if part == nil {
|
if part == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch part := part.(type) {
|
switch part := part.(type) {
|
||||||
case string:
|
case string:
|
||||||
return []Elem{TextElem(part)}
|
return []VDomElem{TextElem(part)}
|
||||||
case *Elem:
|
case *VDomElem:
|
||||||
if part == nil {
|
if part == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return []Elem{*part}
|
return []VDomElem{*part}
|
||||||
case Elem:
|
case VDomElem:
|
||||||
return []Elem{part}
|
return []VDomElem{part}
|
||||||
case []Elem:
|
case []VDomElem:
|
||||||
return part
|
return part
|
||||||
case []*Elem:
|
case []*VDomElem:
|
||||||
var rtn []Elem
|
var rtn []VDomElem
|
||||||
for _, e := range part {
|
for _, e := range part {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
continue
|
continue
|
||||||
@ -235,11 +231,11 @@ func partToElems(part any) []Elem {
|
|||||||
}
|
}
|
||||||
sval, ok := numToString(part)
|
sval, ok := numToString(part)
|
||||||
if ok {
|
if ok {
|
||||||
return []Elem{TextElem(sval)}
|
return []VDomElem{TextElem(sval)}
|
||||||
}
|
}
|
||||||
partVal := reflect.ValueOf(part)
|
partVal := reflect.ValueOf(part)
|
||||||
if partVal.Kind() == reflect.Slice {
|
if partVal.Kind() == reflect.Slice {
|
||||||
var rtn []Elem
|
var rtn []VDomElem
|
||||||
for i := 0; i < partVal.Len(); i++ {
|
for i := 0; i < partVal.Len(); i++ {
|
||||||
subPart := partVal.Index(i).Interface()
|
subPart := partVal.Index(i).Interface()
|
||||||
rtn = append(rtn, partToElems(subPart)...)
|
rtn = append(rtn, partToElems(subPart)...)
|
||||||
@ -248,14 +244,14 @@ func partToElems(part any) []Elem {
|
|||||||
}
|
}
|
||||||
stringer, ok := part.(fmt.Stringer)
|
stringer, ok := part.(fmt.Stringer)
|
||||||
if ok {
|
if ok {
|
||||||
return []Elem{TextElem(stringer.String())}
|
return []VDomElem{TextElem(stringer.String())}
|
||||||
}
|
}
|
||||||
jsonStr, jsonErr := json.Marshal(part)
|
jsonStr, jsonErr := json.Marshal(part)
|
||||||
if jsonErr == nil {
|
if jsonErr == nil {
|
||||||
return []Elem{TextElem(string(jsonStr))}
|
return []VDomElem{TextElem(string(jsonStr))}
|
||||||
}
|
}
|
||||||
typeText := "invalid:" + reflect.TypeOf(part).String()
|
typeText := "invalid:" + reflect.TypeOf(part).String()
|
||||||
return []Elem{TextElem(typeText)}
|
return []VDomElem{TextElem(typeText)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWaveTag(tag string) bool {
|
func isWaveTag(tag string) bool {
|
||||||
|
@ -13,10 +13,10 @@ type ChildKey struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Component struct {
|
type Component struct {
|
||||||
Id string
|
WaveId string
|
||||||
Tag string
|
Tag string
|
||||||
Key string
|
Key string
|
||||||
Elem *Elem
|
Elem *VDomElem
|
||||||
Mounted bool
|
Mounted bool
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
|
@ -10,11 +10,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wavetermdev/htmltoken"
|
"github.com/wavetermdev/htmltoken"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom/cssparser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// can tokenize and bind HTML to Elems
|
// can tokenize and bind HTML to Elems
|
||||||
|
|
||||||
func appendChildToStack(stack []*Elem, child *Elem) {
|
const Html_BindPrefix = "#bind:"
|
||||||
|
const Html_ParamPrefix = "#param:"
|
||||||
|
const Html_GlobalEventPrefix = "#globalevent"
|
||||||
|
const Html_BindParamTagName = "bindparam"
|
||||||
|
const Html_BindTagName = "bind"
|
||||||
|
|
||||||
|
func appendChildToStack(stack []*VDomElem, child *VDomElem) {
|
||||||
if child == nil {
|
if child == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -25,14 +32,14 @@ func appendChildToStack(stack []*Elem, child *Elem) {
|
|||||||
parent.Children = append(parent.Children, *child)
|
parent.Children = append(parent.Children, *child)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushElemStack(stack []*Elem, elem *Elem) []*Elem {
|
func pushElemStack(stack []*VDomElem, elem *VDomElem) []*VDomElem {
|
||||||
if elem == nil {
|
if elem == nil {
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
return append(stack, elem)
|
return append(stack, elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
func popElemStack(stack []*Elem) []*Elem {
|
func popElemStack(stack []*VDomElem) []*VDomElem {
|
||||||
if len(stack) <= 1 {
|
if len(stack) <= 1 {
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
@ -41,14 +48,14 @@ func popElemStack(stack []*Elem) []*Elem {
|
|||||||
return stack[:len(stack)-1]
|
return stack[:len(stack)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func curElemTag(stack []*Elem) string {
|
func curElemTag(stack []*VDomElem) string {
|
||||||
if len(stack) == 0 {
|
if len(stack) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return stack[len(stack)-1].Tag
|
return stack[len(stack)-1].Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
func finalizeStack(stack []*Elem) *Elem {
|
func finalizeStack(stack []*VDomElem) *VDomElem {
|
||||||
if len(stack) == 0 {
|
if len(stack) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -74,8 +81,38 @@ func getAttr(token htmltoken.Token, key string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenToElem(token htmltoken.Token, data map[string]any) *Elem {
|
func attrToProp(attrVal string, params map[string]any) any {
|
||||||
elem := &Elem{Tag: token.Data}
|
if strings.HasPrefix(attrVal, Html_ParamPrefix) {
|
||||||
|
bindKey := attrVal[len(Html_ParamPrefix):]
|
||||||
|
bindVal, ok := params[bindKey]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bindVal
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(attrVal, Html_BindPrefix) {
|
||||||
|
bindKey := attrVal[len(Html_BindPrefix):]
|
||||||
|
if bindKey == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &VDomBinding{Type: ObjectType_Binding, Bind: bindKey}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(attrVal, Html_GlobalEventPrefix) {
|
||||||
|
splitArr := strings.Split(attrVal, ":")
|
||||||
|
if len(splitArr) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
eventName := splitArr[1]
|
||||||
|
if eventName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &VDomFunc{Type: ObjectType_Func, GlobalEvent: eventName}
|
||||||
|
}
|
||||||
|
return attrVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenToElem(token htmltoken.Token, params map[string]any) *VDomElem {
|
||||||
|
elem := &VDomElem{Tag: token.Data}
|
||||||
if len(token.Attr) > 0 {
|
if len(token.Attr) > 0 {
|
||||||
elem.Props = make(map[string]any)
|
elem.Props = make(map[string]any)
|
||||||
}
|
}
|
||||||
@ -83,16 +120,8 @@ func tokenToElem(token htmltoken.Token, data map[string]any) *Elem {
|
|||||||
if attr.Key == "" || attr.Val == "" {
|
if attr.Key == "" || attr.Val == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(attr.Val, "#bind:") {
|
propVal := attrToProp(attr.Val, params)
|
||||||
bindKey := attr.Val[6:]
|
elem.Props[attr.Key] = propVal
|
||||||
bindVal, ok := data[bindKey]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
elem.Props[attr.Key] = bindVal
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
elem.Props[attr.Key] = attr.Val
|
|
||||||
}
|
}
|
||||||
return elem
|
return elem
|
||||||
}
|
}
|
||||||
@ -177,12 +206,101 @@ func processTextStr(s string) string {
|
|||||||
return strings.TrimSpace(s)
|
return strings.TrimSpace(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Bind(htmlStr string, data map[string]any) *Elem {
|
func makePathStr(elemPath []string) string {
|
||||||
|
return strings.Join(elemPath, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func capitalizeAscii(s string) string {
|
||||||
|
if s == "" || s[0] < 'a' || s[0] > 'z' {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return strings.ToUpper(s[:1]) + s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func toReactName(input string) string {
|
||||||
|
// Check for CSS custom properties (variables) which start with '--'
|
||||||
|
if strings.HasPrefix(input, "--") {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
parts := strings.Split(input, "-")
|
||||||
|
result := ""
|
||||||
|
index := 0
|
||||||
|
if parts[0] == "" && len(parts) > 1 {
|
||||||
|
// handle vendor prefixes
|
||||||
|
prefix := parts[1]
|
||||||
|
if prefix == "ms" {
|
||||||
|
result += "ms"
|
||||||
|
} else {
|
||||||
|
result += capitalizeAscii(prefix)
|
||||||
|
}
|
||||||
|
index = 2 // Skip the empty string and prefix
|
||||||
|
} else {
|
||||||
|
result += parts[0]
|
||||||
|
index = 1
|
||||||
|
}
|
||||||
|
// Convert remaining parts to CamelCase
|
||||||
|
for ; index < len(parts); index++ {
|
||||||
|
if parts[index] != "" {
|
||||||
|
result += capitalizeAscii(parts[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertStyleToReactStyles(styleMap map[string]string, params map[string]any) map[string]any {
|
||||||
|
if len(styleMap) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rtn := make(map[string]any)
|
||||||
|
for key, val := range styleMap {
|
||||||
|
rtn[toReactName(key)] = attrToProp(val, params)
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixStyleAttribute(elem *VDomElem, params map[string]any, elemPath []string) error {
|
||||||
|
styleText, ok := elem.Props["style"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parser := cssparser.MakeParser(styleText)
|
||||||
|
m, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%v (at %s)", err, makePathStr(elemPath))
|
||||||
|
}
|
||||||
|
elem.Props["style"] = convertStyleToReactStyles(m, params)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixupStyleAttributes(elem *VDomElem, params map[string]any, elemPath []string) {
|
||||||
|
if elem == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// call fixStyleAttribute, and walk children
|
||||||
|
elemCountMap := make(map[string]int)
|
||||||
|
if len(elemPath) == 0 {
|
||||||
|
elemPath = append(elemPath, elem.Tag)
|
||||||
|
}
|
||||||
|
fixStyleAttribute(elem, params, elemPath)
|
||||||
|
for i := range elem.Children {
|
||||||
|
child := &elem.Children[i]
|
||||||
|
elemCountMap[child.Tag]++
|
||||||
|
subPath := child.Tag
|
||||||
|
if elemCountMap[child.Tag] > 1 {
|
||||||
|
subPath = fmt.Sprintf("%s[%d]", child.Tag, elemCountMap[child.Tag])
|
||||||
|
}
|
||||||
|
elemPath = append(elemPath, subPath)
|
||||||
|
fixupStyleAttributes(&elem.Children[i], params, elemPath)
|
||||||
|
elemPath = elemPath[:len(elemPath)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bind(htmlStr string, params map[string]any) *VDomElem {
|
||||||
htmlStr = processWhitespace(htmlStr)
|
htmlStr = processWhitespace(htmlStr)
|
||||||
r := strings.NewReader(htmlStr)
|
r := strings.NewReader(htmlStr)
|
||||||
iter := htmltoken.NewTokenizer(r)
|
iter := htmltoken.NewTokenizer(r)
|
||||||
var elemStack []*Elem
|
var elemStack []*VDomElem
|
||||||
elemStack = append(elemStack, &Elem{Tag: FragmentTag})
|
elemStack = append(elemStack, &VDomElem{Tag: FragmentTag})
|
||||||
var tokenErr error
|
var tokenErr error
|
||||||
outer:
|
outer:
|
||||||
for {
|
for {
|
||||||
@ -190,15 +308,15 @@ outer:
|
|||||||
token := iter.Token()
|
token := iter.Token()
|
||||||
switch tokenType {
|
switch tokenType {
|
||||||
case htmltoken.StartTagToken:
|
case htmltoken.StartTagToken:
|
||||||
if token.Data == "bind" {
|
if token.Data == Html_BindTagName || token.Data == Html_BindParamTagName {
|
||||||
tokenErr = errors.New("bind tag must be self closing")
|
tokenErr = errors.New("bind tags must be self closing")
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
elem := tokenToElem(token, data)
|
elem := tokenToElem(token, params)
|
||||||
elemStack = pushElemStack(elemStack, elem)
|
elemStack = pushElemStack(elemStack, elem)
|
||||||
case htmltoken.EndTagToken:
|
case htmltoken.EndTagToken:
|
||||||
if token.Data == "bind" {
|
if token.Data == Html_BindTagName || token.Data == Html_BindParamTagName {
|
||||||
tokenErr = errors.New("bind tag must be self closing")
|
tokenErr = errors.New("bind tags must be self closing")
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
if len(elemStack) <= 1 {
|
if len(elemStack) <= 1 {
|
||||||
@ -211,16 +329,22 @@ outer:
|
|||||||
}
|
}
|
||||||
elemStack = popElemStack(elemStack)
|
elemStack = popElemStack(elemStack)
|
||||||
case htmltoken.SelfClosingTagToken:
|
case htmltoken.SelfClosingTagToken:
|
||||||
if token.Data == "bind" {
|
if token.Data == Html_BindParamTagName {
|
||||||
keyAttr := getAttr(token, "key")
|
keyAttr := getAttr(token, "key")
|
||||||
dataVal := data[keyAttr]
|
dataVal := params[keyAttr]
|
||||||
elemList := partToElems(dataVal)
|
elemList := partToElems(dataVal)
|
||||||
for _, elem := range elemList {
|
for _, elem := range elemList {
|
||||||
appendChildToStack(elemStack, &elem)
|
appendChildToStack(elemStack, &elem)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
elem := tokenToElem(token, data)
|
if token.Data == Html_BindTagName {
|
||||||
|
keyAttr := getAttr(token, "key")
|
||||||
|
binding := &VDomBinding{Type: ObjectType_Binding, Bind: keyAttr}
|
||||||
|
appendChildToStack(elemStack, &VDomElem{Tag: WaveTextTag, Props: map[string]any{"text": binding}})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elem := tokenToElem(token, params)
|
||||||
appendChildToStack(elemStack, elem)
|
appendChildToStack(elemStack, elem)
|
||||||
case htmltoken.TextToken:
|
case htmltoken.TextToken:
|
||||||
if token.Data == "" {
|
if token.Data == "" {
|
||||||
@ -249,5 +373,7 @@ outer:
|
|||||||
errTextElem := TextElem(tokenErr.Error())
|
errTextElem := TextElem(tokenErr.Error())
|
||||||
appendChildToStack(elemStack, &errTextElem)
|
appendChildToStack(elemStack, &errTextElem)
|
||||||
}
|
}
|
||||||
return finalizeStack(elemStack)
|
rtn := finalizeStack(elemStack)
|
||||||
|
fixupStyleAttributes(rtn, params, nil)
|
||||||
|
return rtn
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||||
)
|
)
|
||||||
|
|
||||||
type vdomContextKeyType struct{}
|
type vdomContextKeyType struct{}
|
||||||
@ -22,13 +23,20 @@ type VDomContextVal struct {
|
|||||||
HookIdx int
|
HookIdx int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Atom struct {
|
||||||
|
Val any
|
||||||
|
Dirty bool
|
||||||
|
UsedBy map[string]bool // component waveid -> true
|
||||||
|
}
|
||||||
|
|
||||||
type RootElem struct {
|
type RootElem struct {
|
||||||
OuterCtx context.Context
|
OuterCtx context.Context
|
||||||
Root *Component
|
Root *Component
|
||||||
CFuncs map[string]CFunc
|
CFuncs map[string]CFunc
|
||||||
CompMap map[string]*Component // component id -> component
|
CompMap map[string]*Component // component waveid -> component
|
||||||
EffectWorkQueue []*EffectWorkElem
|
EffectWorkQueue []*EffectWorkElem
|
||||||
NeedsRenderMap map[string]bool
|
NeedsRenderMap map[string]bool
|
||||||
|
Atoms map[string]*Atom
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -57,9 +65,49 @@ func MakeRoot() *RootElem {
|
|||||||
Root: nil,
|
Root: nil,
|
||||||
CFuncs: make(map[string]CFunc),
|
CFuncs: make(map[string]CFunc),
|
||||||
CompMap: make(map[string]*Component),
|
CompMap: make(map[string]*Component),
|
||||||
|
Atoms: make(map[string]*Atom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) GetAtom(name string) *Atom {
|
||||||
|
atom, ok := r.Atoms[name]
|
||||||
|
if !ok {
|
||||||
|
atom = &Atom{UsedBy: make(map[string]bool)}
|
||||||
|
r.Atoms[name] = atom
|
||||||
|
}
|
||||||
|
return atom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) GetAtomVal(name string) any {
|
||||||
|
atom := r.GetAtom(name)
|
||||||
|
return atom.Val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) GetStateSync(full bool) []VDomStateSync {
|
||||||
|
stateSync := make([]VDomStateSync, 0)
|
||||||
|
for atomName, atom := range r.Atoms {
|
||||||
|
if atom.Dirty || full {
|
||||||
|
stateSync = append(stateSync, VDomStateSync{Atom: atomName, Value: atom.Val})
|
||||||
|
atom.Dirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stateSync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) SetAtomVal(name string, val any, markDirty bool) {
|
||||||
|
atom := r.GetAtom(name)
|
||||||
|
if !markDirty {
|
||||||
|
atom.Val = val
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// try to avoid setting the value and marking as dirty if it's the "same"
|
||||||
|
if utilfn.JsonValEqual(val, atom.Val) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
atom.Val = val
|
||||||
|
atom.Dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RootElem) SetOuterCtx(ctx context.Context) {
|
func (r *RootElem) SetOuterCtx(ctx context.Context) {
|
||||||
r.OuterCtx = ctx
|
r.OuterCtx = ctx
|
||||||
}
|
}
|
||||||
@ -68,30 +116,60 @@ func (r *RootElem) RegisterComponent(name string, cfunc CFunc) {
|
|||||||
r.CFuncs[name] = cfunc
|
r.CFuncs[name] = cfunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) Render(elem *Elem) {
|
func (r *RootElem) Render(elem *VDomElem) {
|
||||||
log.Printf("Render %s\n", elem.Tag)
|
log.Printf("Render %s\n", elem.Tag)
|
||||||
r.render(elem, &r.Root)
|
r.render(elem, &r.Root)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) Event(id string, propName string) {
|
func (vdf *VDomFunc) CallFn() {
|
||||||
|
if vdf.Fn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rval := reflect.ValueOf(vdf.Fn)
|
||||||
|
if rval.Kind() != reflect.Func {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rval.Call(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callVDomFn(fnVal any, data any) {
|
||||||
|
if fnVal == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn := fnVal
|
||||||
|
if vdf, ok := fnVal.(*VDomFunc); ok {
|
||||||
|
fn = vdf.Fn
|
||||||
|
}
|
||||||
|
if fn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rval := reflect.ValueOf(fn)
|
||||||
|
if rval.Kind() != reflect.Func {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rtype := rval.Type()
|
||||||
|
if rtype.NumIn() == 0 {
|
||||||
|
rval.Call(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rtype.NumIn() == 1 {
|
||||||
|
rval.Call([]reflect.Value{reflect.ValueOf(data)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) Event(id string, propName string, data any) {
|
||||||
comp := r.CompMap[id]
|
comp := r.CompMap[id]
|
||||||
if comp == nil || comp.Elem == nil {
|
if comp == nil || comp.Elem == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fnVal := comp.Elem.Props[propName]
|
fnVal := comp.Elem.Props[propName]
|
||||||
if fnVal == nil {
|
callVDomFn(fnVal, data)
|
||||||
return
|
|
||||||
}
|
|
||||||
fn, ok := fnVal.(func())
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will be called by the frontend to say the DOM has been mounted
|
// this will be called by the frontend to say the DOM has been mounted
|
||||||
// it will eventually send any updated "refs" to the backend as well
|
// it will eventually send any updated "refs" to the backend as well
|
||||||
func (r *RootElem) runWork() {
|
func (r *RootElem) RunWork() {
|
||||||
workQueue := r.EffectWorkQueue
|
workQueue := r.EffectWorkQueue
|
||||||
r.EffectWorkQueue = nil
|
r.EffectWorkQueue = nil
|
||||||
// first, run effect cleanups
|
// first, run effect cleanups
|
||||||
@ -123,7 +201,7 @@ func (r *RootElem) runWork() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) render(elem *Elem, comp **Component) {
|
func (r *RootElem) render(elem *VDomElem, comp **Component) {
|
||||||
if elem == nil || elem.Tag == "" {
|
if elem == nil || elem.Tag == "" {
|
||||||
r.unmount(comp)
|
r.unmount(comp)
|
||||||
return
|
return
|
||||||
@ -171,13 +249,13 @@ func (r *RootElem) unmount(comp **Component) {
|
|||||||
r.unmount(&child)
|
r.unmount(&child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(r.CompMap, (*comp).Id)
|
delete(r.CompMap, (*comp).WaveId)
|
||||||
*comp = nil
|
*comp = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) createComp(tag string, key string, comp **Component) {
|
func (r *RootElem) createComp(tag string, key string, comp **Component) {
|
||||||
*comp = &Component{Id: uuid.New().String(), Tag: tag, Key: key}
|
*comp = &Component{WaveId: uuid.New().String(), Tag: tag, Key: key}
|
||||||
r.CompMap[(*comp).Id] = *comp
|
r.CompMap[(*comp).WaveId] = *comp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) renderText(text string, comp **Component) {
|
func (r *RootElem) renderText(text string, comp **Component) {
|
||||||
@ -186,7 +264,7 @@ func (r *RootElem) renderText(text string, comp **Component) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) renderChildren(elems []Elem, curChildren []*Component) []*Component {
|
func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []*Component {
|
||||||
newChildren := make([]*Component, len(elems))
|
newChildren := make([]*Component, len(elems))
|
||||||
curCM := make(map[ChildKey]*Component)
|
curCM := make(map[ChildKey]*Component)
|
||||||
usedMap := make(map[*Component]bool)
|
usedMap := make(map[*Component]bool)
|
||||||
@ -217,7 +295,7 @@ func (r *RootElem) renderChildren(elems []Elem, curChildren []*Component) []*Com
|
|||||||
return newChildren
|
return newChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) renderSimple(elem *Elem, comp **Component) {
|
func (r *RootElem) renderSimple(elem *VDomElem, comp **Component) {
|
||||||
if (*comp).Comp != nil {
|
if (*comp).Comp != nil {
|
||||||
r.unmount(&(*comp).Comp)
|
r.unmount(&(*comp).Comp)
|
||||||
}
|
}
|
||||||
@ -243,7 +321,7 @@ func getRenderContext(ctx context.Context) *VDomContextVal {
|
|||||||
return v.(*VDomContextVal)
|
return v.(*VDomContextVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) renderComponent(cfunc CFunc, elem *Elem, comp **Component) {
|
func (r *RootElem) renderComponent(cfunc CFunc, elem *VDomElem, comp **Component) {
|
||||||
if (*comp).Children != nil {
|
if (*comp).Children != nil {
|
||||||
for _, child := range (*comp).Children {
|
for _, child := range (*comp).Children {
|
||||||
r.unmount(&child)
|
r.unmount(&child)
|
||||||
@ -262,11 +340,11 @@ func (r *RootElem) renderComponent(cfunc CFunc, elem *Elem, comp **Component) {
|
|||||||
r.unmount(&(*comp).Comp)
|
r.unmount(&(*comp).Comp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var rtnElem *Elem
|
var rtnElem *VDomElem
|
||||||
if len(rtnElemArr) == 1 {
|
if len(rtnElemArr) == 1 {
|
||||||
rtnElem = &rtnElemArr[0]
|
rtnElem = &rtnElemArr[0]
|
||||||
} else {
|
} else {
|
||||||
rtnElem = &Elem{Tag: FragmentTag, Children: rtnElemArr}
|
rtnElem = &VDomElem{Tag: FragmentTag, Children: rtnElemArr}
|
||||||
}
|
}
|
||||||
r.render(rtnElem, &(*comp).Comp)
|
r.render(rtnElem, &(*comp).Comp)
|
||||||
}
|
}
|
||||||
@ -282,7 +360,7 @@ func convertPropsToVDom(props map[string]any) map[string]any {
|
|||||||
}
|
}
|
||||||
val := reflect.ValueOf(v)
|
val := reflect.ValueOf(v)
|
||||||
if val.Kind() == reflect.Func {
|
if val.Kind() == reflect.Func {
|
||||||
vdomProps[k] = VDomFuncType{FuncType: "server"}
|
vdomProps[k] = VDomFunc{Type: ObjectType_Func}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
vdomProps[k] = v
|
vdomProps[k] = v
|
||||||
@ -290,8 +368,8 @@ func convertPropsToVDom(props map[string]any) map[string]any {
|
|||||||
return vdomProps
|
return vdomProps
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertBaseToVDom(c *Component) *Elem {
|
func convertBaseToVDom(c *Component) *VDomElem {
|
||||||
elem := &Elem{Id: c.Id, Tag: c.Tag}
|
elem := &VDomElem{WaveId: c.WaveId, Tag: c.Tag}
|
||||||
if c.Elem != nil {
|
if c.Elem != nil {
|
||||||
elem.Props = convertPropsToVDom(c.Elem.Props)
|
elem.Props = convertPropsToVDom(c.Elem.Props)
|
||||||
}
|
}
|
||||||
@ -304,12 +382,12 @@ func convertBaseToVDom(c *Component) *Elem {
|
|||||||
return elem
|
return elem
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToVDom(c *Component) *Elem {
|
func convertToVDom(c *Component) *VDomElem {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if c.Tag == TextTag {
|
if c.Tag == TextTag {
|
||||||
return &Elem{Tag: TextTag, Text: c.Text}
|
return &VDomElem{Tag: TextTag, Text: c.Text}
|
||||||
}
|
}
|
||||||
if isBaseTag(c.Tag) {
|
if isBaseTag(c.Tag) {
|
||||||
return convertBaseToVDom(c)
|
return convertBaseToVDom(c)
|
||||||
@ -318,11 +396,11 @@ func convertToVDom(c *Component) *Elem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) makeVDom(comp *Component) *Elem {
|
func (r *RootElem) makeVDom(comp *Component) *VDomElem {
|
||||||
vdomElem := convertToVDom(comp)
|
vdomElem := convertToVDom(comp)
|
||||||
return vdomElem
|
return vdomElem
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootElem) MakeVDom() *Elem {
|
func (r *RootElem) MakeVDom() *VDomElem {
|
||||||
return r.makeVDom(r.Root)
|
return r.makeVDom(r.Root)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ type TestContext struct {
|
|||||||
|
|
||||||
func Page(ctx context.Context, props map[string]any) any {
|
func Page(ctx context.Context, props map[string]any) any {
|
||||||
clicked, setClicked := UseState(ctx, false)
|
clicked, setClicked := UseState(ctx, false)
|
||||||
var clickedDiv *Elem
|
var clickedDiv *VDomElem
|
||||||
if clicked {
|
if clicked {
|
||||||
clickedDiv = Bind(`<div>clicked</div>`, nil)
|
clickedDiv = Bind(`<div>clicked</div>`, nil)
|
||||||
}
|
}
|
||||||
@ -30,8 +30,8 @@ func Page(ctx context.Context, props map[string]any) any {
|
|||||||
`
|
`
|
||||||
<div>
|
<div>
|
||||||
<h1>hello world</h1>
|
<h1>hello world</h1>
|
||||||
<Button onClick="#bind:clickFn">hello</Button>
|
<Button onClick="#param:clickFn">hello</Button>
|
||||||
<bind key="clickedDiv"/>
|
<bindparam key="clickedDiv"/>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
map[string]any{"clickFn": clickFn, "clickedDiv": clickedDiv},
|
map[string]any{"clickFn": clickFn, "clickedDiv": clickedDiv},
|
||||||
@ -39,7 +39,7 @@ func Page(ctx context.Context, props map[string]any) any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Button(ctx context.Context, props map[string]any) any {
|
func Button(ctx context.Context, props map[string]any) any {
|
||||||
ref := UseRef(ctx, nil)
|
ref := UseVDomRef(ctx)
|
||||||
clName, setClName := UseState(ctx, "button")
|
clName, setClName := UseState(ctx, "button")
|
||||||
UseEffect(ctx, func() func() {
|
UseEffect(ctx, func() func() {
|
||||||
fmt.Printf("Button useEffect\n")
|
fmt.Printf("Button useEffect\n")
|
||||||
@ -52,8 +52,8 @@ func Button(ctx context.Context, props map[string]any) any {
|
|||||||
testContext.ButtonId = compId
|
testContext.ButtonId = compId
|
||||||
}
|
}
|
||||||
return Bind(`
|
return Bind(`
|
||||||
<div className="#bind:clName" ref="#bind:ref" onClick="#bind:onClick">
|
<div className="#param:clName" ref="#param:ref" onClick="#param:onClick">
|
||||||
<bind key="children"/>
|
<bindparam key="children"/>
|
||||||
</div>
|
</div>
|
||||||
`, map[string]any{"clName": clName, "ref": ref, "onClick": props["onClick"], "children": props["children"]})
|
`, map[string]any{"clName": clName, "ref": ref, "onClick": props["onClick"], "children": props["children"]})
|
||||||
}
|
}
|
||||||
@ -85,10 +85,10 @@ func Test1(t *testing.T) {
|
|||||||
t.Fatalf("root.Root is nil")
|
t.Fatalf("root.Root is nil")
|
||||||
}
|
}
|
||||||
printVDom(root)
|
printVDom(root)
|
||||||
root.runWork()
|
root.RunWork()
|
||||||
printVDom(root)
|
printVDom(root)
|
||||||
root.Event(testContext.ButtonId, "onClick")
|
root.Event(testContext.ButtonId, "onClick", nil)
|
||||||
root.runWork()
|
root.RunWork()
|
||||||
printVDom(root)
|
printVDom(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,8 +111,8 @@ func TestBind(t *testing.T) {
|
|||||||
elem = Bind(`
|
elem = Bind(`
|
||||||
<div>
|
<div>
|
||||||
<h1>hello world</h1>
|
<h1>hello world</h1>
|
||||||
<Button onClick="#bind:clickFn">hello</Button>
|
<Button onClick="#param:clickFn">hello</Button>
|
||||||
<bind key="clickedDiv"/>
|
<bindparam key="clickedDiv"/>
|
||||||
</div>
|
</div>
|
||||||
`, nil)
|
`, nil)
|
||||||
jsonBytes, _ = json.MarshalIndent(elem, "", " ")
|
jsonBytes, _ = json.MarshalIndent(elem, "", " ")
|
||||||
|
195
pkg/vdom/vdom_types.go
Normal file
195
pkg/vdom/vdom_types.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package vdom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TextTag = "#text"
|
||||||
|
const WaveTextTag = "wave:text"
|
||||||
|
const WaveNullTag = "wave:null"
|
||||||
|
const FragmentTag = "#fragment"
|
||||||
|
const BindTag = "#bind"
|
||||||
|
|
||||||
|
const ChildrenPropKey = "children"
|
||||||
|
const KeyPropKey = "key"
|
||||||
|
|
||||||
|
const ObjectType_Ref = "ref"
|
||||||
|
const ObjectType_Binding = "binding"
|
||||||
|
const ObjectType_Func = "func"
|
||||||
|
|
||||||
|
// vdom element
|
||||||
|
type VDomElem struct {
|
||||||
|
WaveId string `json:"waveid,omitempty"` // required, except for #text nodes
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Props map[string]any `json:"props,omitempty"`
|
||||||
|
Children []VDomElem `json:"children,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//// protocol messages
|
||||||
|
|
||||||
|
type VDomCreateContext struct {
|
||||||
|
Type string `json:"type" tstype:"\"createcontext\""`
|
||||||
|
Ts int64 `json:"ts"`
|
||||||
|
Meta waveobj.MetaMapType `json:"meta,omitempty"`
|
||||||
|
NewBlock bool `json:"newblock,omitempty"`
|
||||||
|
Persist bool `json:"persist,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomAsyncInitiationRequest struct {
|
||||||
|
Type string `json:"type" tstype:"\"asyncinitiationrequest\""`
|
||||||
|
Ts int64 `json:"ts"`
|
||||||
|
BlockId string `json:"blockid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeAsyncInitiationRequest(blockId string) VDomAsyncInitiationRequest {
|
||||||
|
return VDomAsyncInitiationRequest{
|
||||||
|
Type: "asyncinitiationrequest",
|
||||||
|
Ts: time.Now().UnixMilli(),
|
||||||
|
BlockId: blockId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomFrontendUpdate struct {
|
||||||
|
Type string `json:"type" tstype:"\"frontendupdate\""`
|
||||||
|
Ts int64 `json:"ts"`
|
||||||
|
BlockId string `json:"blockid"`
|
||||||
|
CorrelationId string `json:"correlationid,omitempty"`
|
||||||
|
Initialize bool `json:"initialize,omitempty"` // initialize the app
|
||||||
|
Dispose bool `json:"dispose,omitempty"` // the vdom context was closed
|
||||||
|
Resync bool `json:"resync,omitempty"` // resync (send all backend data). useful when the FE reloads
|
||||||
|
RenderContext VDomRenderContext `json:"rendercontext,omitempty"`
|
||||||
|
Events []VDomEvent `json:"events,omitempty"`
|
||||||
|
StateSync []VDomStateSync `json:"statesync,omitempty"`
|
||||||
|
RefUpdates []VDomRefUpdate `json:"refupdates,omitempty"`
|
||||||
|
Messages []VDomMessage `json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomBackendUpdate struct {
|
||||||
|
Type string `json:"type" tstype:"\"backendupdate\""`
|
||||||
|
Ts int64 `json:"ts"`
|
||||||
|
BlockId string `json:"blockid"`
|
||||||
|
Opts *VDomBackendOpts `json:"opts,omitempty"`
|
||||||
|
RenderUpdates []VDomRenderUpdate `json:"renderupdates,omitempty"`
|
||||||
|
StateSync []VDomStateSync `json:"statesync,omitempty"`
|
||||||
|
RefOperations []VDomRefOperation `json:"refoperations,omitempty"`
|
||||||
|
Messages []VDomMessage `json:"messages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
///// prop types
|
||||||
|
|
||||||
|
// used in props
|
||||||
|
type VDomBinding struct {
|
||||||
|
Type string `json:"type" tstype:"\"binding\""`
|
||||||
|
Bind string `json:"bind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// used in props
|
||||||
|
type VDomFunc struct {
|
||||||
|
Fn any `json:"-"` // server side function (called with reflection)
|
||||||
|
Type string `json:"type" tstype:"\"func\""`
|
||||||
|
StopPropagation bool `json:"stoppropagation,omitempty"`
|
||||||
|
PreventDefault bool `json:"preventdefault,omitempty"`
|
||||||
|
GlobalEvent string `json:"globalevent,omitempty"`
|
||||||
|
Keys []string `json:"keys,omitempty"` // special for keyDown events a list of keys to "capture"
|
||||||
|
}
|
||||||
|
|
||||||
|
// used in props
|
||||||
|
type VDomRef struct {
|
||||||
|
Type string `json:"type" tstype:"\"ref\""`
|
||||||
|
RefId string `json:"refid"`
|
||||||
|
TrackPosition bool `json:"trackposition,omitempty"`
|
||||||
|
Position *VDomRefPosition `json:"position,omitempty"`
|
||||||
|
HasCurrent bool `json:"hascurrent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomRect struct {
|
||||||
|
Top float64 `json:"top"`
|
||||||
|
Left float64 `json:"left"`
|
||||||
|
Right float64 `json:"right"`
|
||||||
|
Bottom float64 `json:"bottom"`
|
||||||
|
Width float64 `json:"width"`
|
||||||
|
Height float64 `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomRefPosition struct {
|
||||||
|
OffsetHeight int `json:"offsetheight"`
|
||||||
|
OffsetWidth int `json:"offsetwidth"`
|
||||||
|
ScrollHeight int `json:"scrollheight"`
|
||||||
|
ScrollWidth int `json:"scrollwidth"`
|
||||||
|
ScrollTop int `json:"scrolltop"`
|
||||||
|
BoundingClientRect DomRect `json:"boundingclientrect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
///// subbordinate protocol types
|
||||||
|
|
||||||
|
type VDomEvent struct {
|
||||||
|
WaveId string `json:"waveid"`
|
||||||
|
PropName string `json:"propname"`
|
||||||
|
EventData any `json:"eventdata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomRenderContext struct {
|
||||||
|
BlockId string `json:"blockid"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
RootRefId string `json:"rootrefid"`
|
||||||
|
Background bool `json:"background,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomStateSync struct {
|
||||||
|
Atom string `json:"atom"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomRefUpdate struct {
|
||||||
|
RefId string `json:"refid"`
|
||||||
|
HasCurrent bool `json:"hascurrent"`
|
||||||
|
Position *VDomRefPosition `json:"position,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomBackendOpts struct {
|
||||||
|
CloseOnCtrlC bool `json:"closeonctrlc,omitempty"`
|
||||||
|
GlobalKeyboardEvents bool `json:"globalkeyboardevents,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomRenderUpdate struct {
|
||||||
|
UpdateType string `json:"updatetype" tstype:"\"root\"|\"append\"|\"replace\"|\"remove\"|\"insert\""`
|
||||||
|
WaveId string `json:"waveid,omitempty"`
|
||||||
|
VDom VDomElem `json:"vdom"`
|
||||||
|
Index *int `json:"index,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomRefOperation struct {
|
||||||
|
RefId string `json:"refid"`
|
||||||
|
Op string `json:"op" tsype:"\"focus\""`
|
||||||
|
Params []any `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomMessage struct {
|
||||||
|
MessageType string `json:"messagetype"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
StackTrace string `json:"stacktrace,omitempty"`
|
||||||
|
Params []any `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches WaveKeyboardEvent
|
||||||
|
type VDomKeyboardEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Shift bool `json:"shift,omitempty"`
|
||||||
|
Control bool `json:"ctrl,omitempty"`
|
||||||
|
Alt bool `json:"alt,omitempty"`
|
||||||
|
Meta bool `json:"meta,omitempty"`
|
||||||
|
Cmd bool `json:"cmd,omitempty"`
|
||||||
|
Option bool `json:"option,omitempty"`
|
||||||
|
Repeat bool `json:"repeat,omitempty"`
|
||||||
|
Location int `json:"location,omitempty"`
|
||||||
|
}
|
199
pkg/vdom/vdomclient/vdomclient.go
Normal file
199
pkg/vdom/vdomclient/vdomclient.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package vdomclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Root *vdom.RootElem
|
||||||
|
RootElem *vdom.VDomElem
|
||||||
|
RpcClient *wshutil.WshRpc
|
||||||
|
RpcContext *wshrpc.RpcContext
|
||||||
|
ServerImpl *VDomServerImpl
|
||||||
|
IsDone bool
|
||||||
|
RouteId string
|
||||||
|
DoneReason string
|
||||||
|
DoneOnce *sync.Once
|
||||||
|
DoneCh chan struct{}
|
||||||
|
Opts vdom.VDomBackendOpts
|
||||||
|
GlobalEventHandler func(client *Client, event vdom.VDomEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomServerImpl struct {
|
||||||
|
Client *Client
|
||||||
|
BlockId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*VDomServerImpl) WshServerImpl() {}
|
||||||
|
|
||||||
|
func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error) {
|
||||||
|
if feUpdate.Dispose {
|
||||||
|
log.Printf("got dispose from frontend\n")
|
||||||
|
impl.Client.doShutdown("got dispose from frontend")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if impl.Client.IsDone {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// set atoms
|
||||||
|
for _, ss := range feUpdate.StateSync {
|
||||||
|
impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
|
||||||
|
}
|
||||||
|
// run events
|
||||||
|
for _, event := range feUpdate.Events {
|
||||||
|
if event.WaveId == "" {
|
||||||
|
if impl.Client.GlobalEventHandler != nil {
|
||||||
|
impl.Client.GlobalEventHandler(impl.Client, event)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
impl.Client.Root.Event(event.WaveId, event.PropName, event.EventData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if feUpdate.Initialize || feUpdate.Resync {
|
||||||
|
return impl.Client.fullRender()
|
||||||
|
}
|
||||||
|
return impl.Client.incrementalRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doShutdown(reason string) {
|
||||||
|
c.DoneOnce.Do(func() {
|
||||||
|
c.DoneReason = reason
|
||||||
|
c.IsDone = true
|
||||||
|
close(c.DoneCh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetGlobalEventHandler(handler func(client *Client, event vdom.VDomEvent)) {
|
||||||
|
c.GlobalEventHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
|
||||||
|
client := &Client{
|
||||||
|
Root: vdom.MakeRoot(),
|
||||||
|
DoneCh: make(chan struct{}),
|
||||||
|
DoneOnce: &sync.Once{},
|
||||||
|
}
|
||||||
|
if opts != nil {
|
||||||
|
client.Opts = *opts
|
||||||
|
}
|
||||||
|
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
|
||||||
|
if jwtToken == "" {
|
||||||
|
return nil, fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName)
|
||||||
|
}
|
||||||
|
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
|
||||||
|
}
|
||||||
|
client.RpcContext = rpcCtx
|
||||||
|
if client.RpcContext == nil || client.RpcContext.BlockId == "" {
|
||||||
|
return nil, fmt.Errorf("no block id in rpc context")
|
||||||
|
}
|
||||||
|
client.ServerImpl = &VDomServerImpl{BlockId: client.RpcContext.BlockId, Client: client}
|
||||||
|
sockName, err := wshutil.ExtractUnverifiedSocketName(jwtToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting socket name from %s: %v", wshutil.WaveJwtTokenVarName, err)
|
||||||
|
}
|
||||||
|
rpcClient, err := wshutil.SetupDomainSocketRpcClient(sockName, client.ServerImpl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error setting up domain socket rpc client: %v", err)
|
||||||
|
}
|
||||||
|
client.RpcClient = rpcClient
|
||||||
|
authRtn, err := wshclient.AuthenticateCommand(client.RpcClient, jwtToken, &wshrpc.RpcOpts{NoResponse: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error authenticating rpc connection: %v", err)
|
||||||
|
}
|
||||||
|
client.RouteId = authRtn.RouteId
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetRootElem(elem *vdom.VDomElem) {
|
||||||
|
c.RootElem = elem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateVDomContext() error {
|
||||||
|
err := wshclient.VDomCreateContextCommand(c.RpcClient, vdom.VDomCreateContext{}, &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{
|
||||||
|
waveobj.MakeORef("block", c.RpcContext.BlockId).String(),
|
||||||
|
}}, nil)
|
||||||
|
c.RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
|
||||||
|
c.doShutdown("got blockclose event")
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SendAsyncInitiation() {
|
||||||
|
wshclient.VDomAsyncInitiationCommand(c.RpcClient, vdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId), &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.RpcContext.BlockId)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetAtomVals(m map[string]any) {
|
||||||
|
for k, v := range m {
|
||||||
|
c.Root.SetAtomVal(k, v, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SetAtomVal(name string, val any) {
|
||||||
|
c.Root.SetAtomVal(name, val, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAtomVal(name string) any {
|
||||||
|
return c.Root.GetAtomVal(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNullVDom() *vdom.VDomElem {
|
||||||
|
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
|
||||||
|
c.Root.RunWork()
|
||||||
|
c.Root.Render(c.RootElem)
|
||||||
|
renderedVDom := c.Root.MakeVDom()
|
||||||
|
if renderedVDom == nil {
|
||||||
|
renderedVDom = makeNullVDom()
|
||||||
|
}
|
||||||
|
return &vdom.VDomBackendUpdate{
|
||||||
|
Type: "backendupdate",
|
||||||
|
Ts: time.Now().UnixMilli(),
|
||||||
|
BlockId: c.RpcContext.BlockId,
|
||||||
|
Opts: &c.Opts,
|
||||||
|
RenderUpdates: []vdom.VDomRenderUpdate{
|
||||||
|
{UpdateType: "root", VDom: *renderedVDom},
|
||||||
|
},
|
||||||
|
StateSync: c.Root.GetStateSync(true),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
|
||||||
|
c.Root.RunWork()
|
||||||
|
renderedVDom := c.Root.MakeVDom()
|
||||||
|
if renderedVDom == nil {
|
||||||
|
renderedVDom = makeNullVDom()
|
||||||
|
}
|
||||||
|
return &vdom.VDomBackendUpdate{
|
||||||
|
Type: "backendupdate",
|
||||||
|
Ts: time.Now().UnixMilli(),
|
||||||
|
BlockId: c.RpcContext.BlockId,
|
||||||
|
RenderUpdates: []vdom.VDomRenderUpdate{
|
||||||
|
{UpdateType: "root", VDom: *renderedVDom},
|
||||||
|
},
|
||||||
|
StateSync: c.Root.GetStateSync(false),
|
||||||
|
}, nil
|
||||||
|
}
|
@ -78,6 +78,10 @@ const (
|
|||||||
MetaKey_TermLocalShellOpts = "term:localshellopts"
|
MetaKey_TermLocalShellOpts = "term:localshellopts"
|
||||||
MetaKey_TermScrollback = "term:scrollback"
|
MetaKey_TermScrollback = "term:scrollback"
|
||||||
|
|
||||||
|
MetaKey_VDomClear = "vdom:*"
|
||||||
|
MetaKey_VDomInitialized = "vdom:initialized"
|
||||||
|
MetaKey_VDomCorrelationId = "vdom:correlationid"
|
||||||
|
|
||||||
MetaKey_Count = "count"
|
MetaKey_Count = "count"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,6 +79,10 @@ type MetaTSType struct {
|
|||||||
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
|
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
|
||||||
TermScrollback *int `json:"term:scrollback,omitempty"`
|
TermScrollback *int `json:"term:scrollback,omitempty"`
|
||||||
|
|
||||||
|
VDomClear bool `json:"vdom:*,omitempty"`
|
||||||
|
VDomInitialized bool `json:"vdom:initialized,omitempty"`
|
||||||
|
VDomCorrelationId string `json:"vdom:correlationid,omitempty"`
|
||||||
|
|
||||||
Count int `json:"count,omitempty"` // temp for cpu plot. will remove later
|
Count int `json:"count,omitempty"` // temp for cpu plot. will remove later
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ const (
|
|||||||
Event_BlockFile = "blockfile"
|
Event_BlockFile = "blockfile"
|
||||||
Event_Config = "config"
|
Event_Config = "config"
|
||||||
Event_UserInput = "userinput"
|
Event_UserInput = "userinput"
|
||||||
|
Event_RouteGone = "route:gone"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WaveEvent struct {
|
type WaveEvent struct {
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// command "authenticate", wshserver.AuthenticateCommand
|
// command "authenticate", wshserver.AuthenticateCommand
|
||||||
@ -260,6 +261,24 @@ func TestCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "vdomasyncinitiation", wshserver.VDomAsyncInitiationCommand
|
||||||
|
func VDomAsyncInitiationCommand(w *wshutil.WshRpc, data vdom.VDomAsyncInitiationRequest, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "vdomasyncinitiation", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "vdomcreatecontext", wshserver.VDomCreateContextCommand
|
||||||
|
func VDomCreateContextCommand(w *wshutil.WshRpc, data vdom.VDomCreateContext, opts *wshrpc.RpcOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "vdomcreatecontext", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "vdomrender", wshserver.VDomRenderCommand
|
||||||
|
func VDomRenderCommand(w *wshutil.WshRpc, data vdom.VDomFrontendUpdate, opts *wshrpc.RpcOpts) (*vdom.VDomBackendUpdate, error) {
|
||||||
|
resp, err := sendRpcRequestCallHelper[*vdom.VDomBackendUpdate](w, "vdomrender", data, opts)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
// command "webselector", wshserver.WebSelectorCommand
|
// command "webselector", wshserver.WebSelectorCommand
|
||||||
func WebSelectorCommand(w *wshutil.WshRpc, data wshrpc.CommandWebSelectorData, opts *wshrpc.RpcOpts) ([]string, error) {
|
func WebSelectorCommand(w *wshutil.WshRpc, data wshrpc.CommandWebSelectorData, opts *wshrpc.RpcOpts) ([]string, error) {
|
||||||
resp, err := sendRpcRequestCallHelper[[]string](w, "webselector", data, opts)
|
resp, err := sendRpcRequestCallHelper[[]string](w, "webselector", data, opts)
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/wavetermdev/waveterm/pkg/ijson"
|
"github.com/wavetermdev/waveterm/pkg/ijson"
|
||||||
|
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||||
@ -69,6 +70,10 @@ const (
|
|||||||
|
|
||||||
Command_WebSelector = "webselector"
|
Command_WebSelector = "webselector"
|
||||||
Command_Notify = "notify"
|
Command_Notify = "notify"
|
||||||
|
|
||||||
|
Command_VDomCreateContext = "vdomcreatecontext"
|
||||||
|
Command_VDomAsyncInitiation = "vdomasyncinitiation"
|
||||||
|
Command_VDomRender = "vdomrender"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RespOrErrorUnion[T any] struct {
|
type RespOrErrorUnion[T any] struct {
|
||||||
@ -126,8 +131,16 @@ type WshRpcInterface interface {
|
|||||||
RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error)
|
RemoteFileJoinCommand(ctx context.Context, paths []string) (*FileInfo, error)
|
||||||
RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData]
|
RemoteStreamCpuDataCommand(ctx context.Context) chan RespOrErrorUnion[TimeSeriesData]
|
||||||
|
|
||||||
|
// emain
|
||||||
WebSelectorCommand(ctx context.Context, data CommandWebSelectorData) ([]string, error)
|
WebSelectorCommand(ctx context.Context, data CommandWebSelectorData) ([]string, error)
|
||||||
NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error
|
NotifyCommand(ctx context.Context, notificationOptions WaveNotificationOptions) error
|
||||||
|
|
||||||
|
// terminal
|
||||||
|
VDomCreateContextCommand(ctx context.Context, data vdom.VDomCreateContext) error
|
||||||
|
VDomAsyncInitiationCommand(ctx context.Context, data vdom.VDomAsyncInitiationRequest) error
|
||||||
|
|
||||||
|
// proc
|
||||||
|
VDomRenderCommand(ctx context.Context, data vdom.VDomFrontendUpdate) (*vdom.VDomBackendUpdate, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// for frontend
|
// for frontend
|
||||||
|
@ -60,6 +60,10 @@ func MakeTabRouteId(tabId string) string {
|
|||||||
return "tab:" + tabId
|
return "tab:" + tabId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeFeBlockRouteId(blockId string) string {
|
||||||
|
return "feblock:" + blockId
|
||||||
|
}
|
||||||
|
|
||||||
var DefaultRouter = NewWshRouter()
|
var DefaultRouter = NewWshRouter()
|
||||||
|
|
||||||
func NewWshRouter() *WshRouter {
|
func NewWshRouter() *WshRouter {
|
||||||
@ -322,6 +326,7 @@ func (router *WshRouter) UnregisterRoute(routeId string) {
|
|||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
wps.Broker.UnsubscribeAll(routeId)
|
wps.Broker.UnsubscribeAll(routeId)
|
||||||
|
wps.Broker.Publish(wps.WaveEvent{Event: wps.Event_RouteGone, Scopes: []string{routeId}})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user