mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-21 02:33:34 +01:00
VDom 8 (#1202)
* new vdomevents to support click, change, keydown, etc. easier type signature * can now pass a prop type instead of always converting to map[string]any * implement DefineComponent to allow easier vdom creation using a component function directly * separate vdomclient Make from Connect * lots of bug fixes to get everything working again * PStyle and special "style" attribute handling
This commit is contained in:
parent
3f5bb6d12c
commit
91c293e4be
@ -6,7 +6,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
@ -18,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var htmlCmdNewBlock bool
|
||||
var GlobalVDomClient *vdomclient.Client
|
||||
var HtmlVDomClient *vdomclient.Client = vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
|
||||
func init() {
|
||||
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
|
||||
@ -32,172 +31,198 @@ var htmlCmd = &cobra.Command{
|
||||
RunE: htmlRun,
|
||||
}
|
||||
|
||||
func StyleTag(ctx context.Context, props map[string]any) any {
|
||||
return vdom.Bind(`
|
||||
<style>
|
||||
.root {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.background-inner {
|
||||
max-width: 300px;
|
||||
|
||||
.bg-item {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-grey-hover-bg);
|
||||
}
|
||||
|
||||
.bg-preview {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
|
||||
.bg-label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`, nil)
|
||||
}
|
||||
|
||||
// Prop Types
|
||||
type BgItemProps struct {
|
||||
Bg string
|
||||
Label string
|
||||
Bg string `json:"bg"`
|
||||
Label string `json:"label"`
|
||||
OnClick func() `json:"onClick"`
|
||||
}
|
||||
|
||||
func BgItemTag(ctx context.Context, props BgItemProps) any {
|
||||
clickFn := func() {
|
||||
log.Printf("bg item clicked %q\n", props.Bg)
|
||||
blockInfo, err := wshclient.BlockInfoCommand(GlobalVDomClient.RpcClient, GlobalVDomClient.RpcContext.BlockId, nil)
|
||||
if err != nil {
|
||||
log.Printf("error getting block info: %v\n", err)
|
||||
return
|
||||
type BgListProps struct {
|
||||
Items []BgItem `json:"items"`
|
||||
}
|
||||
|
||||
type BgItem struct {
|
||||
Bg string `json:"bg"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// Components
|
||||
var Style = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "Style",
|
||||
func(ctx context.Context, _ struct{}) any {
|
||||
return vdom.E("style", nil, `
|
||||
.root {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.background-inner {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.bg-item {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bg-item:hover {
|
||||
background-color: var(--button-grey-hover-bg);
|
||||
}
|
||||
|
||||
.bg-preview {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
|
||||
.bg-label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`)
|
||||
},
|
||||
)
|
||||
|
||||
var BgItemTag = vdomclient.DefineComponent[BgItemProps](HtmlVDomClient, "BgItem",
|
||||
func(ctx context.Context, props BgItemProps) any {
|
||||
return vdom.E("div",
|
||||
vdom.P("className", "bg-item"),
|
||||
vdom.P("onClick", props.OnClick),
|
||||
vdom.E("div",
|
||||
vdom.P("className", "bg-preview"),
|
||||
vdom.PStyle("background", props.Bg),
|
||||
),
|
||||
vdom.E("div",
|
||||
vdom.P("className", "bg-label"),
|
||||
props.Label,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
var BgList = vdomclient.DefineComponent[BgListProps](HtmlVDomClient, "BgList",
|
||||
func(ctx context.Context, props BgListProps) any {
|
||||
setBackground := func(bg string) func() {
|
||||
return func() {
|
||||
blockInfo, err := wshclient.BlockInfoCommand(HtmlVDomClient.RpcClient, HtmlVDomClient.RpcContext.BlockId, nil)
|
||||
if err != nil {
|
||||
log.Printf("error getting block info: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = wshclient.SetMetaCommand(HtmlVDomClient.RpcClient, wshrpc.CommandSetMetaData{
|
||||
ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId},
|
||||
Meta: map[string]any{"bg": bg},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Printf("error setting meta: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("block info: tabid=%q\n", blockInfo.TabId)
|
||||
err = wshclient.SetMetaCommand(GlobalVDomClient.RpcClient, wshrpc.CommandSetMetaData{
|
||||
ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId},
|
||||
Meta: map[string]any{"bg": props.Bg},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
log.Printf("error setting meta: %v\n", err)
|
||||
|
||||
items := make([]*vdom.VDomElem, 0, len(props.Items))
|
||||
for _, item := range props.Items {
|
||||
items = append(items, BgItemTag(BgItemProps{
|
||||
Bg: item.Bg,
|
||||
Label: item.Label,
|
||||
OnClick: setBackground(item.Bg),
|
||||
}))
|
||||
}
|
||||
// wshclient.SetMetaCommand(GlobalVDomClient.RpcClient)
|
||||
}
|
||||
params := map[string]any{
|
||||
"bg": props.Bg,
|
||||
"label": props.Label,
|
||||
"clickHandler": clickFn,
|
||||
}
|
||||
return vdom.Bind(`
|
||||
<div className="bg-item" onClick="#param:clickHandler">
|
||||
<div className="bg-preview" style="background: #param:bg"></div>
|
||||
<div className="bg-label"><bindparam key="label"/></div>
|
||||
</div>`, params)
|
||||
}
|
||||
|
||||
func AllBgItemsTag(ctx context.Context, props map[string]any) any {
|
||||
items := []map[string]any{
|
||||
{"bg": nil, "label": "default"},
|
||||
{"bg": "#ff0000", "label": "red"},
|
||||
{"bg": "#00ff00", "label": "green"},
|
||||
{"bg": "#0000ff", "label": "blue"},
|
||||
}
|
||||
bgElems := make([]*vdom.VDomElem, 0)
|
||||
for _, item := range items {
|
||||
elem := vdom.E("BgItemTag", item)
|
||||
bgElems = append(bgElems, elem)
|
||||
}
|
||||
return vdom.Bind(`
|
||||
<div className="background">
|
||||
<div className="background-inner">
|
||||
<bindparam key="bgElems"/>
|
||||
</div>
|
||||
</div>
|
||||
`, map[string]any{"bgElems": bgElems})
|
||||
}
|
||||
return vdom.E("div",
|
||||
vdom.P("className", "background"),
|
||||
vdom.E("div",
|
||||
vdom.P("className", "background-inner"),
|
||||
items,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
func MakeVDom() *vdom.VDomElem {
|
||||
vdomStr := `
|
||||
<div className="root">
|
||||
<StyleTag/>
|
||||
<h1>Set Background</h1>
|
||||
<div>
|
||||
<wave:markdown text="*quick vdom application to set background colors*"/>
|
||||
</div>
|
||||
<div>
|
||||
<AllBgItemsTag/>
|
||||
</div>
|
||||
<div>
|
||||
<img style="width: 100%; height: 100%; max-width: 300px; max-height: 300px; object-fit: contain;" src="vdom:///test.png"/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
elem := vdom.Bind(vdomStr, nil)
|
||||
return elem
|
||||
}
|
||||
var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App",
|
||||
func(ctx context.Context, _ struct{}) any {
|
||||
inputText, setInputText := vdom.UseState(ctx, "start")
|
||||
|
||||
func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) {
|
||||
if event.EventType == "clickinc" {
|
||||
client.SetAtomVal("num", client.GetAtomVal("num").(int)+1)
|
||||
return
|
||||
}
|
||||
}
|
||||
bgItems := []BgItem{
|
||||
{Bg: "", Label: "default"},
|
||||
{Bg: "#ff0000", Label: "red"},
|
||||
{Bg: "#00ff00", Label: "green"},
|
||||
{Bg: "#0000ff", Label: "blue"},
|
||||
}
|
||||
|
||||
return vdom.E("div",
|
||||
vdom.P("className", "root"),
|
||||
Style(struct{}{}),
|
||||
vdom.E("h1", nil, "Set Background"),
|
||||
vdom.E("div", nil,
|
||||
vdom.E("wave:markdown",
|
||||
vdom.P("text", "*quick vdom application to set background colors*"),
|
||||
),
|
||||
),
|
||||
vdom.E("div", nil,
|
||||
BgList(BgListProps{Items: bgItems}),
|
||||
),
|
||||
vdom.E("div", nil,
|
||||
vdom.E("img",
|
||||
vdom.P("style", "width: 100%; height: 100%; max-width: 300px; max-height: 300px; object-fit: contain;"),
|
||||
vdom.P("src", "vdom:///test.png"),
|
||||
),
|
||||
),
|
||||
vdom.E("div", nil,
|
||||
vdom.E("input",
|
||||
vdom.P("type", "text"),
|
||||
vdom.P("value", inputText),
|
||||
vdom.P("onChange", func(e vdom.VDomEvent) {
|
||||
setInputText(e.TargetValue)
|
||||
}),
|
||||
),
|
||||
vdom.E("div", nil,
|
||||
"text ", inputText,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
func htmlRun(cmd *cobra.Command, args []string) error {
|
||||
WriteStderr("running wsh html %q\n", RpcContext.BlockId)
|
||||
|
||||
client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
|
||||
client := HtmlVDomClient
|
||||
err := client.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
client.SetGlobalEventHandler(GlobalEventHandler)
|
||||
log.Printf("created client: %v\n", client)
|
||||
client.RegisterComponent("StyleTag", StyleTag)
|
||||
client.RegisterComponent("BgItemTag", BgItemTag)
|
||||
client.RegisterComponent("AllBgItemsTag", AllBgItemsTag)
|
||||
|
||||
// Set up the root component
|
||||
client.SetRootElem(App(struct{}{}))
|
||||
|
||||
// Set up file handler
|
||||
client.RegisterFileHandler("/test.png", "~/Downloads/IMG_1939.png")
|
||||
client.SetRootElem(MakeVDom())
|
||||
|
||||
// Create the VDOM context
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("created context\n")
|
||||
|
||||
// Handle shutdown
|
||||
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)
|
||||
log.Printf("updating text\n")
|
||||
client.SetAtomVal("text", "updated text")
|
||||
err := client.SendAsyncInitiation()
|
||||
if err != nil {
|
||||
log.Printf("error sending async initiation: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-client.DoneCh
|
||||
return nil
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { BlockNodeModel } from "@/app/block/blocktypes";
|
||||
import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global";
|
||||
import { getBlockMetaKeyAtom, globalStore, PLATFORM, WOS } from "@/app/store/global";
|
||||
import { makeORef } from "@/app/store/wos";
|
||||
import { waveEventSubscribe } from "@/app/store/wps";
|
||||
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
|
||||
@ -43,24 +43,45 @@ function makeVDomIdMap(vdom: VDomElem, idMap: Map<string, VDomElem>) {
|
||||
}
|
||||
}
|
||||
|
||||
function convertEvent(e: React.SyntheticEvent, fromProp: string): any {
|
||||
if (e == null) {
|
||||
return null;
|
||||
function annotateEvent(event: VDomEvent, propName: string, reactEvent: React.SyntheticEvent) {
|
||||
if (reactEvent == null) {
|
||||
return;
|
||||
}
|
||||
if (fromProp == "onClick") {
|
||||
return { type: "click" };
|
||||
if (propName == "onChange") {
|
||||
const changeEvent = reactEvent as React.ChangeEvent<any>;
|
||||
event.targetvalue = changeEvent.target?.value;
|
||||
event.targetchecked = changeEvent.target?.checked;
|
||||
}
|
||||
if (fromProp == "onKeyDown") {
|
||||
const waveKeyEvent = adaptFromReactOrNativeKeyEvent(e as React.KeyboardEvent);
|
||||
return waveKeyEvent;
|
||||
if (propName == "onClick" || propName == "onMouseDown") {
|
||||
const mouseEvent = reactEvent as React.MouseEvent<any>;
|
||||
event.mousedata = {
|
||||
button: mouseEvent.button,
|
||||
buttons: mouseEvent.buttons,
|
||||
alt: mouseEvent.altKey,
|
||||
control: mouseEvent.ctrlKey,
|
||||
shift: mouseEvent.shiftKey,
|
||||
meta: mouseEvent.metaKey,
|
||||
clientx: mouseEvent.clientX,
|
||||
clienty: mouseEvent.clientY,
|
||||
pagex: mouseEvent.pageX,
|
||||
pagey: mouseEvent.pageY,
|
||||
screenx: mouseEvent.screenX,
|
||||
screeny: mouseEvent.screenY,
|
||||
movementx: mouseEvent.movementX,
|
||||
movementy: mouseEvent.movementY,
|
||||
};
|
||||
if (PLATFORM == "darwin") {
|
||||
event.mousedata.cmd = event.mousedata.meta;
|
||||
event.mousedata.option = event.mousedata.alt;
|
||||
} else {
|
||||
event.mousedata.cmd = event.mousedata.alt;
|
||||
event.mousedata.option = event.mousedata.meta;
|
||||
}
|
||||
}
|
||||
if (fromProp == "onFocus") {
|
||||
return { type: "focus" };
|
||||
if (propName == "onKeyDown") {
|
||||
const waveKeyEvent = adaptFromReactOrNativeKeyEvent(reactEvent as React.KeyboardEvent);
|
||||
event.keydata = waveKeyEvent;
|
||||
}
|
||||
if (fromProp == "onBlur") {
|
||||
return { type: "blur" };
|
||||
}
|
||||
return { type: "unknown" };
|
||||
}
|
||||
|
||||
class VDomWshClient extends WshClient {
|
||||
@ -200,7 +221,7 @@ export class VDomModel {
|
||||
this.batchedEvents.push({
|
||||
waveid: null,
|
||||
eventtype: "onKeyDown",
|
||||
eventdata: e,
|
||||
keydata: e,
|
||||
});
|
||||
this.queueUpdate();
|
||||
return true;
|
||||
@ -540,26 +561,22 @@ export class VDomModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (update.haswork) {
|
||||
this.queueUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
callVDomFunc(fnDecl: VDomFunc, e: any, compId: string, propName: string) {
|
||||
const eventData = convertEvent(e, propName);
|
||||
callVDomFunc(fnDecl: VDomFunc, e: React.SyntheticEvent, compId: string, propName: string) {
|
||||
const vdomEvent: VDomEvent = {
|
||||
waveid: compId,
|
||||
eventtype: propName,
|
||||
};
|
||||
if (fnDecl.globalevent) {
|
||||
const waveEvent: VDomEvent = {
|
||||
waveid: null,
|
||||
eventtype: fnDecl.globalevent,
|
||||
eventdata: eventData,
|
||||
};
|
||||
this.batchedEvents.push(waveEvent);
|
||||
} else {
|
||||
const vdomEvent: VDomEvent = {
|
||||
waveid: compId,
|
||||
eventtype: propName,
|
||||
eventdata: eventData,
|
||||
};
|
||||
this.batchedEvents.push(vdomEvent);
|
||||
vdomEvent.globaleventtype = fnDecl.globalevent;
|
||||
}
|
||||
this.queueUpdate();
|
||||
annotateEvent(vdomEvent, propName, e);
|
||||
this.batchedEvents.push(vdomEvent);
|
||||
this.queueUpdate(true);
|
||||
}
|
||||
|
||||
createFeUpdate(): VDomFrontendUpdate {
|
||||
|
@ -136,3 +136,49 @@ export function validateAndWrapReactStyle(model: VDomModel, style: Record<string
|
||||
}
|
||||
return sanitizedStyle;
|
||||
}
|
||||
|
||||
type VDomTransferElem = {
|
||||
root?: boolean;
|
||||
waveid?: string;
|
||||
tag: string;
|
||||
props?: { [key: string]: any };
|
||||
children?: string[]; // References to child WaveIds
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export function UnmarshalTransferElems(transferElems: VDomTransferElem[]): VDomElem[] {
|
||||
const elemMap: { [id: string]: VDomElem } = {};
|
||||
const roots: VDomElem[] = [];
|
||||
|
||||
// Initialize each VDomTransferElem in the map without children, as we'll link them after
|
||||
transferElems.forEach((transferElem) => {
|
||||
if (!transferElem.waveid) {
|
||||
return; // Skip elements without waveid
|
||||
}
|
||||
const elem: VDomElem = {
|
||||
waveid: transferElem.tag !== "#text" ? transferElem.waveid : undefined,
|
||||
tag: transferElem.tag,
|
||||
props: transferElem.props,
|
||||
text: transferElem.text,
|
||||
children: [], // Placeholder to be populated later
|
||||
};
|
||||
elemMap[transferElem.waveid] = elem;
|
||||
|
||||
// Collect root elements
|
||||
if (transferElem.root) {
|
||||
roots.push(elem);
|
||||
}
|
||||
});
|
||||
|
||||
// Now populate children for each element
|
||||
transferElems.forEach((transferElem) => {
|
||||
if (!transferElem.waveid || !transferElem.children) return;
|
||||
|
||||
const currentElem = elemMap[transferElem.waveid];
|
||||
currentElem.children = transferElem.children
|
||||
.map((childId) => elemMap[childId])
|
||||
.filter((child) => child !== undefined); // Filter out any undefined children
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
@ -145,8 +145,9 @@ const SvgUrlIdAttributes = {
|
||||
function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void {
|
||||
return (e: any) => {
|
||||
if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) {
|
||||
dlog("key event", fnDecl, e);
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
for (let keyDesc of fnDecl.keys || []) {
|
||||
for (let keyDesc of fnDecl["#keys"] || []) {
|
||||
if (checkKeyPressed(waveEvent, keyDesc)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
42
frontend/types/custom.d.ts
vendored
42
frontend/types/custom.d.ts
vendored
@ -134,48 +134,6 @@ declare global {
|
||||
keyType: string;
|
||||
};
|
||||
|
||||
interface WaveKeyboardEvent {
|
||||
type: "keydown" | "keyup" | "keypress" | "unknown";
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.key.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.code.
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.shiftKey.
|
||||
*/
|
||||
shift: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.controlKey.
|
||||
*/
|
||||
control: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.altKey.
|
||||
*/
|
||||
alt: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.metaKey.
|
||||
*/
|
||||
meta: boolean;
|
||||
/**
|
||||
* cmd is special, on mac it is meta, on windows it is alt
|
||||
*/
|
||||
cmd: boolean;
|
||||
/**
|
||||
* option is special, on mac it is alt, on windows it is meta
|
||||
*/
|
||||
option: boolean;
|
||||
|
||||
repeat: boolean;
|
||||
/**
|
||||
* Equivalent to KeyboardEvent.location.
|
||||
*/
|
||||
location: number;
|
||||
}
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
|
||||
type HeaderElem =
|
||||
|
46
frontend/types/gotypes.d.ts
vendored
46
frontend/types/gotypes.d.ts
vendored
@ -650,6 +650,7 @@ declare global {
|
||||
ts: number;
|
||||
blockid: string;
|
||||
opts?: VDomBackendOpts;
|
||||
haswork?: boolean;
|
||||
renderupdates?: VDomRenderUpdate[];
|
||||
statesync?: VDomStateSync[];
|
||||
refoperations?: VDomRefOperation[];
|
||||
@ -684,7 +685,13 @@ declare global {
|
||||
type VDomEvent = {
|
||||
waveid: string;
|
||||
eventtype: string;
|
||||
eventdata: any;
|
||||
globaleventtype?: string;
|
||||
targetvalue?: string;
|
||||
targetchecked?: boolean;
|
||||
targetname?: string;
|
||||
targetid?: string;
|
||||
keydata?: WaveKeyboardEvent;
|
||||
mousedata?: WavePointerData;
|
||||
};
|
||||
|
||||
// vdom.VDomFrontendUpdate
|
||||
@ -708,7 +715,7 @@ declare global {
|
||||
stoppropagation?: boolean;
|
||||
preventdefault?: boolean;
|
||||
globalevent?: string;
|
||||
keys?: string[];
|
||||
#keys?: string[];
|
||||
};
|
||||
|
||||
// vdom.VDomMessage
|
||||
@ -855,6 +862,21 @@ declare global {
|
||||
datadir: string;
|
||||
};
|
||||
|
||||
// vdom.WaveKeyboardEvent
|
||||
type WaveKeyboardEvent = {
|
||||
type: "keydown"|"keyup"|"keypress"|"unknown";
|
||||
key: string;
|
||||
code: string;
|
||||
repeat?: boolean;
|
||||
location?: number;
|
||||
shift?: boolean;
|
||||
control?: boolean;
|
||||
alt?: boolean;
|
||||
meta?: boolean;
|
||||
cmd?: boolean;
|
||||
option?: boolean;
|
||||
};
|
||||
|
||||
// wshrpc.WaveNotificationOptions
|
||||
type WaveNotificationOptions = {
|
||||
title?: string;
|
||||
@ -878,6 +900,26 @@ declare global {
|
||||
obj?: WaveObj;
|
||||
};
|
||||
|
||||
// vdom.WavePointerData
|
||||
type WavePointerData = {
|
||||
button: number;
|
||||
buttons: number;
|
||||
clientx?: number;
|
||||
clienty?: number;
|
||||
pagex?: number;
|
||||
pagey?: number;
|
||||
screenx?: number;
|
||||
screeny?: number;
|
||||
movementx?: number;
|
||||
movementy?: number;
|
||||
shift?: boolean;
|
||||
control?: boolean;
|
||||
alt?: boolean;
|
||||
meta?: boolean;
|
||||
cmd?: boolean;
|
||||
option?: boolean;
|
||||
};
|
||||
|
||||
// waveobj.Window
|
||||
type WaveWindow = WaveObj & {
|
||||
workspaceid: string;
|
||||
|
145
pkg/util/utilfn/marshal.go
Normal file
145
pkg/util/utilfn/marshal.go
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utilfn
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func ReUnmarshal(out any, in any) error {
|
||||
barr, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(barr, out)
|
||||
}
|
||||
|
||||
// does a mapstructure using "json" tags
|
||||
func DoMapStructure(out any, input any) error {
|
||||
dconfig := &mapstructure.DecoderConfig{
|
||||
Result: out,
|
||||
TagName: "json",
|
||||
}
|
||||
decoder, err := mapstructure.NewDecoder(dconfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decoder.Decode(input)
|
||||
}
|
||||
|
||||
func MapToStruct(in map[string]any, out any) error {
|
||||
// Check that out is a pointer
|
||||
outValue := reflect.ValueOf(out)
|
||||
if outValue.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("out parameter must be a pointer, got %v", outValue.Kind())
|
||||
}
|
||||
|
||||
// Get the struct it points to
|
||||
elem := outValue.Elem()
|
||||
if elem.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("out parameter must be a pointer to struct, got pointer to %v", elem.Kind())
|
||||
}
|
||||
|
||||
// Get type information
|
||||
typ := elem.Type()
|
||||
|
||||
// For each field in the struct
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
|
||||
// Skip unexported fields
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := getJSONName(field)
|
||||
if value, ok := in[name]; ok {
|
||||
if err := setValue(elem.Field(i), value); err != nil {
|
||||
return fmt.Errorf("error setting field %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StructToMap(in any) (map[string]any, error) {
|
||||
// Get value and handle pointer
|
||||
val := reflect.ValueOf(in)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
// Check that we have a struct
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("input must be a struct or pointer to struct, got %v", val.Kind())
|
||||
}
|
||||
|
||||
// Get type information
|
||||
typ := val.Type()
|
||||
out := make(map[string]any)
|
||||
|
||||
// For each field in the struct
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
|
||||
// Skip unexported fields
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := getJSONName(field)
|
||||
out[name] = val.Field(i).Interface()
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// getJSONName returns the field name to use for JSON mapping
|
||||
func getJSONName(field reflect.StructField) string {
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "" || tag == "-" {
|
||||
return field.Name
|
||||
}
|
||||
return strings.Split(tag, ",")[0]
|
||||
}
|
||||
|
||||
// setValue attempts to set a reflect.Value with a given interface{} value
|
||||
func setValue(field reflect.Value, value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
valueRef := reflect.ValueOf(value)
|
||||
|
||||
// Direct assignment if types are exactly equal
|
||||
if valueRef.Type() == field.Type() {
|
||||
field.Set(valueRef)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if types are assignable
|
||||
if valueRef.Type().AssignableTo(field.Type()) {
|
||||
field.Set(valueRef)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If field is pointer and value isn't already a pointer, try address
|
||||
if field.Kind() == reflect.Ptr && valueRef.Kind() != reflect.Ptr {
|
||||
return setValue(field, valueRef.Addr().Interface())
|
||||
}
|
||||
|
||||
// Try conversion if types are convertible
|
||||
if valueRef.Type().ConvertibleTo(field.Type()) {
|
||||
field.Set(valueRef.Convert(field.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot set value of type %v to field of type %v", valueRef.Type(), field.Type())
|
||||
}
|
@ -29,8 +29,6 @@ import (
|
||||
"syscall"
|
||||
"text/template"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
|
||||
@ -751,27 +749,6 @@ func IndentString(indent string, str string) string {
|
||||
return rtn.String()
|
||||
}
|
||||
|
||||
func ReUnmarshal(out any, in any) error {
|
||||
barr, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(barr, out)
|
||||
}
|
||||
|
||||
// does a mapstructure using "json" tags
|
||||
func DoMapStructure(out any, input any) error {
|
||||
dconfig := &mapstructure.DecoderConfig{
|
||||
Result: out,
|
||||
TagName: "json",
|
||||
}
|
||||
decoder, err := mapstructure.NewDecoder(dconfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decoder.Decode(input)
|
||||
}
|
||||
|
||||
func SliceIdx[T comparable](arr []T, elem T) int {
|
||||
for idx, e := range arr {
|
||||
if e == elem {
|
||||
|
@ -7,10 +7,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
)
|
||||
|
||||
// ReactNode types = nil | string | Elem
|
||||
@ -25,6 +28,17 @@ type Hook struct {
|
||||
Deps []any
|
||||
}
|
||||
|
||||
type Component[P any] func(props P) *VDomElem
|
||||
|
||||
type styleAttrWrapper struct {
|
||||
StyleAttr string
|
||||
Val any
|
||||
}
|
||||
|
||||
type styleAttrMapWrapper struct {
|
||||
StyleAttrMap map[string]any
|
||||
}
|
||||
|
||||
func (e *VDomElem) Key() string {
|
||||
keyVal, ok := e.Props[KeyPropKey]
|
||||
if !ok {
|
||||
@ -54,6 +68,20 @@ func mergeProps(props *map[string]any, newProps map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
func mergeStyleAttr(props *map[string]any, styleAttr styleAttrWrapper) {
|
||||
if *props == nil {
|
||||
*props = make(map[string]any)
|
||||
}
|
||||
if (*props)["style"] == nil {
|
||||
(*props)["style"] = make(map[string]any)
|
||||
}
|
||||
styleMap, ok := (*props)["style"].(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
styleMap[styleAttr.StyleAttr] = styleAttr.Val
|
||||
}
|
||||
|
||||
func E(tag string, parts ...any) *VDomElem {
|
||||
rtn := &VDomElem{Tag: tag}
|
||||
for _, part := range parts {
|
||||
@ -65,13 +93,49 @@ func E(tag string, parts ...any) *VDomElem {
|
||||
mergeProps(&rtn.Props, props)
|
||||
continue
|
||||
}
|
||||
if styleAttr, ok := part.(styleAttrWrapper); ok {
|
||||
mergeStyleAttr(&rtn.Props, styleAttr)
|
||||
continue
|
||||
}
|
||||
if styleAttrMap, ok := part.(styleAttrMapWrapper); ok {
|
||||
for k, v := range styleAttrMap.StyleAttrMap {
|
||||
mergeStyleAttr(&rtn.Props, styleAttrWrapper{StyleAttr: k, Val: v})
|
||||
}
|
||||
continue
|
||||
}
|
||||
elems := partToElems(part)
|
||||
rtn.Children = append(rtn.Children, elems...)
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
func P(propName string, propVal any) map[string]any {
|
||||
func Props(props any) map[string]any {
|
||||
m, err := utilfn.StructToMap(props)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func PStyle(styleAttr string, propVal any) any {
|
||||
return styleAttrWrapper{StyleAttr: styleAttr, Val: propVal}
|
||||
}
|
||||
|
||||
func P(propName string, propVal any) any {
|
||||
if propVal == nil {
|
||||
return map[string]any{propName: nil}
|
||||
}
|
||||
if propName == "style" {
|
||||
strVal, ok := propVal.(string)
|
||||
if ok {
|
||||
styleMap, err := styleAttrStrToStyleMap(strVal, nil)
|
||||
if err == nil {
|
||||
return styleAttrMapWrapper{StyleAttrMap: styleMap}
|
||||
}
|
||||
log.Printf("Error parsing style attribute: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return map[string]any{propName: propVal}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ type ChildKey struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
type Component struct {
|
||||
type ComponentImpl struct {
|
||||
WaveId string
|
||||
Tag string
|
||||
Key string
|
||||
@ -26,13 +26,13 @@ type Component struct {
|
||||
Text string
|
||||
|
||||
// base component -- vdom, wave elem, or #fragment
|
||||
Children []*Component
|
||||
Children []*ComponentImpl
|
||||
|
||||
// component -> component
|
||||
Comp *Component
|
||||
Comp *ComponentImpl
|
||||
}
|
||||
|
||||
func (c *Component) compMatch(tag string, key string) bool {
|
||||
func (c *ComponentImpl) compMatch(tag string, key string) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
@ -273,17 +273,25 @@ func convertStyleToReactStyles(styleMap map[string]string, params map[string]any
|
||||
return rtn
|
||||
}
|
||||
|
||||
func styleAttrStrToStyleMap(styleText string, params map[string]any) (map[string]any, error) {
|
||||
parser := cssparser.MakeParser(styleText)
|
||||
m, err := parser.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertStyleToReactStyles(m, params), nil
|
||||
}
|
||||
|
||||
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()
|
||||
styleMap, err := styleAttrStrToStyleMap(styleText, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v (at %s)", err, makePathStr(elemPath))
|
||||
}
|
||||
elem.Props["style"] = convertStyleToReactStyles(m, params)
|
||||
elem.Props["style"] = styleMap
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@ package vdom
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -19,7 +18,7 @@ var vdomContextKey = vdomContextKeyType{}
|
||||
|
||||
type VDomContextVal struct {
|
||||
Root *RootElem
|
||||
Comp *Component
|
||||
Comp *ComponentImpl
|
||||
HookIdx int
|
||||
}
|
||||
|
||||
@ -31,9 +30,9 @@ type Atom struct {
|
||||
|
||||
type RootElem struct {
|
||||
OuterCtx context.Context
|
||||
Root *Component
|
||||
Root *ComponentImpl
|
||||
CFuncs map[string]any
|
||||
CompMap map[string]*Component // component waveid -> component
|
||||
CompMap map[string]*ComponentImpl // component waveid -> component
|
||||
EffectWorkQueue []*EffectWorkElem
|
||||
NeedsRenderMap map[string]bool
|
||||
Atoms map[string]*Atom
|
||||
@ -64,7 +63,7 @@ func MakeRoot() *RootElem {
|
||||
return &RootElem{
|
||||
Root: nil,
|
||||
CFuncs: make(map[string]any),
|
||||
CompMap: make(map[string]*Component),
|
||||
CompMap: make(map[string]*ComponentImpl),
|
||||
Atoms: make(map[string]*Atom),
|
||||
}
|
||||
}
|
||||
@ -151,11 +150,10 @@ func (r *RootElem) RegisterComponent(name string, cfunc any) error {
|
||||
}
|
||||
|
||||
func (r *RootElem) Render(elem *VDomElem) {
|
||||
log.Printf("Render %s\n", elem.Tag)
|
||||
r.render(elem, &r.Root)
|
||||
}
|
||||
|
||||
func (vdf *VDomFunc) CallFn() {
|
||||
func (vdf *VDomFunc) CallFn(event VDomEvent) {
|
||||
if vdf.Fn == nil {
|
||||
return
|
||||
}
|
||||
@ -163,10 +161,18 @@ func (vdf *VDomFunc) CallFn() {
|
||||
if rval.Kind() != reflect.Func {
|
||||
return
|
||||
}
|
||||
rval.Call(nil)
|
||||
rtype := rval.Type()
|
||||
if rtype.NumIn() == 0 {
|
||||
rval.Call(nil)
|
||||
}
|
||||
if rtype.NumIn() == 1 {
|
||||
if rtype.In(0) == reflect.TypeOf((*VDomEvent)(nil)).Elem() {
|
||||
rval.Call([]reflect.Value{reflect.ValueOf(event)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func callVDomFn(fnVal any, data any) {
|
||||
func callVDomFn(fnVal any, data VDomEvent) {
|
||||
if fnVal == nil {
|
||||
return
|
||||
}
|
||||
@ -192,13 +198,13 @@ func callVDomFn(fnVal any, data any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootElem) Event(id string, propName string, data any) {
|
||||
func (r *RootElem) Event(id string, propName string, event VDomEvent) {
|
||||
comp := r.CompMap[id]
|
||||
if comp == nil || comp.Elem == nil {
|
||||
return
|
||||
}
|
||||
fnVal := comp.Elem.Props[propName]
|
||||
callVDomFn(fnVal, data)
|
||||
callVDomFn(fnVal, event)
|
||||
}
|
||||
|
||||
// this will be called by the frontend to say the DOM has been mounted
|
||||
@ -235,7 +241,7 @@ func (r *RootElem) RunWork() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootElem) render(elem *VDomElem, comp **Component) {
|
||||
func (r *RootElem) render(elem *VDomElem, comp **ComponentImpl) {
|
||||
if elem == nil || elem.Tag == "" {
|
||||
r.unmount(comp)
|
||||
return
|
||||
@ -264,7 +270,7 @@ func (r *RootElem) render(elem *VDomElem, comp **Component) {
|
||||
r.renderComponent(cfunc, elem, comp)
|
||||
}
|
||||
|
||||
func (r *RootElem) unmount(comp **Component) {
|
||||
func (r *RootElem) unmount(comp **ComponentImpl) {
|
||||
if *comp == nil {
|
||||
return
|
||||
}
|
||||
@ -287,21 +293,21 @@ func (r *RootElem) unmount(comp **Component) {
|
||||
*comp = nil
|
||||
}
|
||||
|
||||
func (r *RootElem) createComp(tag string, key string, comp **Component) {
|
||||
*comp = &Component{WaveId: uuid.New().String(), Tag: tag, Key: key}
|
||||
func (r *RootElem) createComp(tag string, key string, comp **ComponentImpl) {
|
||||
*comp = &ComponentImpl{WaveId: uuid.New().String(), Tag: tag, Key: key}
|
||||
r.CompMap[(*comp).WaveId] = *comp
|
||||
}
|
||||
|
||||
func (r *RootElem) renderText(text string, comp **Component) {
|
||||
func (r *RootElem) renderText(text string, comp **ComponentImpl) {
|
||||
if (*comp).Text != text {
|
||||
(*comp).Text = text
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []*Component {
|
||||
newChildren := make([]*Component, len(elems))
|
||||
curCM := make(map[ChildKey]*Component)
|
||||
usedMap := make(map[*Component]bool)
|
||||
func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*ComponentImpl) []*ComponentImpl {
|
||||
newChildren := make([]*ComponentImpl, len(elems))
|
||||
curCM := make(map[ChildKey]*ComponentImpl)
|
||||
usedMap := make(map[*ComponentImpl]bool)
|
||||
for idx, child := range curChildren {
|
||||
if child.Key != "" {
|
||||
curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child
|
||||
@ -311,7 +317,7 @@ func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []
|
||||
}
|
||||
for idx, elem := range elems {
|
||||
elemKey := elem.Key()
|
||||
var curChild *Component
|
||||
var curChild *ComponentImpl
|
||||
if elemKey != "" {
|
||||
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}]
|
||||
} else {
|
||||
@ -329,14 +335,14 @@ func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []
|
||||
return newChildren
|
||||
}
|
||||
|
||||
func (r *RootElem) renderSimple(elem *VDomElem, comp **Component) {
|
||||
func (r *RootElem) renderSimple(elem *VDomElem, comp **ComponentImpl) {
|
||||
if (*comp).Comp != nil {
|
||||
r.unmount(&(*comp).Comp)
|
||||
}
|
||||
(*comp).Children = r.renderChildren(elem.Children, (*comp).Children)
|
||||
}
|
||||
|
||||
func (r *RootElem) makeRenderContext(comp *Component) context.Context {
|
||||
func (r *RootElem) makeRenderContext(comp *ComponentImpl) context.Context {
|
||||
var ctx context.Context
|
||||
if r.OuterCtx != nil {
|
||||
ctx = r.OuterCtx
|
||||
@ -359,7 +365,15 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
|
||||
rval := reflect.ValueOf(cfunc)
|
||||
arg2Type := rval.Type().In(1)
|
||||
arg2Val := reflect.New(arg2Type)
|
||||
utilfn.ReUnmarshal(arg2Val.Interface(), props)
|
||||
// if arg2 is a map, just pass props
|
||||
if arg2Type.Kind() == reflect.Map {
|
||||
arg2Val.Elem().Set(reflect.ValueOf(props))
|
||||
} else {
|
||||
err := utilfn.MapToStruct(props, arg2Val.Interface())
|
||||
if err != nil {
|
||||
fmt.Printf("error unmarshalling props: %v\n", err)
|
||||
}
|
||||
}
|
||||
rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()})
|
||||
if len(rtnVal) == 0 {
|
||||
return nil
|
||||
@ -367,7 +381,7 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
|
||||
return rtnVal[0].Interface()
|
||||
}
|
||||
|
||||
func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **Component) {
|
||||
func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentImpl) {
|
||||
if (*comp).Children != nil {
|
||||
for _, child := range (*comp).Children {
|
||||
r.unmount(&child)
|
||||
@ -414,7 +428,7 @@ func convertPropsToVDom(props map[string]any) map[string]any {
|
||||
return vdomProps
|
||||
}
|
||||
|
||||
func convertBaseToVDom(c *Component) *VDomElem {
|
||||
func convertBaseToVDom(c *ComponentImpl) *VDomElem {
|
||||
elem := &VDomElem{WaveId: c.WaveId, Tag: c.Tag}
|
||||
if c.Elem != nil {
|
||||
elem.Props = convertPropsToVDom(c.Elem.Props)
|
||||
@ -428,7 +442,7 @@ func convertBaseToVDom(c *Component) *VDomElem {
|
||||
return elem
|
||||
}
|
||||
|
||||
func convertToVDom(c *Component) *VDomElem {
|
||||
func convertToVDom(c *ComponentImpl) *VDomElem {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@ -442,7 +456,7 @@ func convertToVDom(c *Component) *VDomElem {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RootElem) makeVDom(comp *Component) *VDomElem {
|
||||
func (r *RootElem) makeVDom(comp *ComponentImpl) *VDomElem {
|
||||
vdomElem := convertToVDom(comp)
|
||||
return vdomElem
|
||||
}
|
||||
@ -450,3 +464,53 @@ func (r *RootElem) makeVDom(comp *Component) *VDomElem {
|
||||
func (r *RootElem) MakeVDom() *VDomElem {
|
||||
return r.makeVDom(r.Root)
|
||||
}
|
||||
|
||||
func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem {
|
||||
var transferElems []VDomTransferElem
|
||||
textCounter := 0 // Counter for generating unique IDs for #text nodes
|
||||
|
||||
// Helper function to recursively process each VDomElem in preorder
|
||||
var processElem func(elem VDomElem, isRoot bool) string
|
||||
processElem = func(elem VDomElem, isRoot bool) string {
|
||||
// Handle #text nodes by generating a unique placeholder ID
|
||||
if elem.Tag == "#text" {
|
||||
textId := fmt.Sprintf("text-%d", textCounter)
|
||||
textCounter++
|
||||
transferElems = append(transferElems, VDomTransferElem{
|
||||
Root: isRoot,
|
||||
WaveId: textId,
|
||||
Tag: elem.Tag,
|
||||
Text: elem.Text,
|
||||
Props: nil,
|
||||
Children: nil,
|
||||
})
|
||||
return textId
|
||||
}
|
||||
|
||||
// Convert children to WaveId references, handling potential #text nodes
|
||||
childrenIds := make([]string, len(elem.Children))
|
||||
for i, child := range elem.Children {
|
||||
childrenIds[i] = processElem(child, false) // Children are not roots
|
||||
}
|
||||
|
||||
// Create the VDomTransferElem for the current element
|
||||
transferElem := VDomTransferElem{
|
||||
Root: isRoot,
|
||||
WaveId: elem.WaveId,
|
||||
Tag: elem.Tag,
|
||||
Props: elem.Props,
|
||||
Children: childrenIds,
|
||||
Text: elem.Text,
|
||||
}
|
||||
transferElems = append(transferElems, transferElem)
|
||||
|
||||
return elem.WaveId
|
||||
}
|
||||
|
||||
// Start processing each top-level element, marking them as roots
|
||||
for _, elem := range elems {
|
||||
processElem(elem, true)
|
||||
}
|
||||
|
||||
return transferElems
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func Test1(t *testing.T) {
|
||||
printVDom(root)
|
||||
root.RunWork()
|
||||
printVDom(root)
|
||||
root.Event(testContext.ButtonId, "onClick", nil)
|
||||
root.Event(testContext.ButtonId, "onClick", VDomEvent{EventType: "onClick"})
|
||||
root.RunWork()
|
||||
printVDom(root)
|
||||
}
|
||||
|
@ -31,6 +31,16 @@ type VDomElem struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// the over the wire format for a vdom element
|
||||
type VDomTransferElem struct {
|
||||
Root bool `json:"root,omitempty"`
|
||||
WaveId string `json:"waveid,omitempty"` // required, except for #text nodes
|
||||
Tag string `json:"tag"`
|
||||
Props map[string]any `json:"props,omitempty"`
|
||||
Children []string `json:"children,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
//// protocol messages
|
||||
|
||||
type VDomCreateContext struct {
|
||||
@ -74,6 +84,7 @@ type VDomBackendUpdate struct {
|
||||
Ts int64 `json:"ts"`
|
||||
BlockId string `json:"blockid"`
|
||||
Opts *VDomBackendOpts `json:"opts,omitempty"`
|
||||
HasWork bool `json:"haswork,omitempty"`
|
||||
RenderUpdates []VDomRenderUpdate `json:"renderupdates,omitempty"`
|
||||
StateSync []VDomStateSync `json:"statesync,omitempty"`
|
||||
RefOperations []VDomRefOperation `json:"refoperations,omitempty"`
|
||||
@ -95,7 +106,7 @@ type VDomFunc struct {
|
||||
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"
|
||||
Keys []string `json:"#keys,omitempty"` // special for keyDown events a list of keys to "capture"
|
||||
}
|
||||
|
||||
// used in props
|
||||
@ -128,9 +139,15 @@ type VDomRefPosition struct {
|
||||
///// subbordinate protocol types
|
||||
|
||||
type VDomEvent struct {
|
||||
WaveId string `json:"waveid"` // empty for global events
|
||||
EventType string `json:"eventtype"`
|
||||
EventData any `json:"eventdata"`
|
||||
WaveId string `json:"waveid"`
|
||||
EventType string `json:"eventtype"` // usually the prop name (e.g. onClick, onKeyDown)
|
||||
GlobalEventType string `json:"globaleventtype,omitempty"`
|
||||
TargetValue string `json:"targetvalue,omitempty"`
|
||||
TargetChecked bool `json:"targetchecked,omitempty"`
|
||||
TargetName string `json:"targetname,omitempty"`
|
||||
TargetId string `json:"targetid,omitempty"`
|
||||
KeyData *WaveKeyboardEvent `json:"keydata,omitempty"`
|
||||
MouseData *WavePointerData `json:"mousedata,omitempty"`
|
||||
}
|
||||
|
||||
type VDomRenderContext struct {
|
||||
@ -199,3 +216,41 @@ type VDomKeyboardEvent struct {
|
||||
Repeat bool `json:"repeat,omitempty"`
|
||||
Location int `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
type WaveKeyboardEvent struct {
|
||||
Type string `json:"type" tstype:"\"keydown\"|\"keyup\"|\"keypress\"|\"unknown\""`
|
||||
Key string `json:"key"` // KeyboardEvent.key
|
||||
Code string `json:"code"` // KeyboardEvent.code
|
||||
Repeat bool `json:"repeat,omitempty"`
|
||||
Location int `json:"location,omitempty"` // KeyboardEvent.location
|
||||
|
||||
// modifiers
|
||||
Shift bool `json:"shift,omitempty"`
|
||||
Control bool `json:"control,omitempty"`
|
||||
Alt bool `json:"alt,omitempty"`
|
||||
Meta bool `json:"meta,omitempty"`
|
||||
Cmd bool `json:"cmd,omitempty"` // special (on mac it is meta, on windows/linux it is alt)
|
||||
Option bool `json:"option,omitempty"` // special (on mac it is alt, on windows/linux it is meta)
|
||||
}
|
||||
|
||||
type WavePointerData struct {
|
||||
Button int `json:"button"`
|
||||
Buttons int `json:"buttons"`
|
||||
|
||||
ClientX int `json:"clientx,omitempty"`
|
||||
ClientY int `json:"clienty,omitempty"`
|
||||
PageX int `json:"pagex,omitempty"`
|
||||
PageY int `json:"pagey,omitempty"`
|
||||
ScreenX int `json:"screenx,omitempty"`
|
||||
ScreenY int `json:"screeny,omitempty"`
|
||||
MovementX int `json:"movementx,omitempty"`
|
||||
MovementY int `json:"movementy,omitempty"`
|
||||
|
||||
// Modifiers
|
||||
Shift bool `json:"shift,omitempty"`
|
||||
Control bool `json:"control,omitempty"`
|
||||
Alt bool `json:"alt,omitempty"`
|
||||
Meta bool `json:"meta,omitempty"`
|
||||
Cmd bool `json:"cmd,omitempty"` // special (on mac it is meta, on windows/linux it is alt)
|
||||
Option bool `json:"option,omitempty"` // special (on mac it is alt, on windows/linux it is meta)
|
||||
}
|
||||
|
@ -4,12 +4,14 @@
|
||||
package vdomclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
@ -64,7 +66,7 @@ func (c *Client) SetOverrideUrlHandler(handler http.Handler) {
|
||||
c.OverrideUrlHandler = handler
|
||||
}
|
||||
|
||||
func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
|
||||
func MakeClient(opts *vdom.VDomBackendOpts) *Client {
|
||||
client := &Client{
|
||||
Lock: &sync.Mutex{},
|
||||
Root: vdom.MakeRoot(),
|
||||
@ -74,34 +76,38 @@ func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
|
||||
if opts != nil {
|
||||
client.Opts = *opts
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (client *Client) Connect() error {
|
||||
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
|
||||
if jwtToken == "" {
|
||||
return nil, fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName)
|
||||
return 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)
|
||||
return 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")
|
||||
return 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)
|
||||
return 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)
|
||||
return 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)
|
||||
return fmt.Errorf("error authenticating rpc connection: %v", err)
|
||||
}
|
||||
client.RouteId = authRtn.RouteId
|
||||
return client, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SetRootElem(elem *vdom.VDomElem) {
|
||||
@ -170,6 +176,19 @@ func makeNullVDom() *vdom.VDomElem {
|
||||
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
|
||||
}
|
||||
|
||||
func DefineComponent[P any](client *Client, name string, renderFn func(ctx context.Context, props P) any) vdom.Component[P] {
|
||||
if name == "" {
|
||||
panic("Component name cannot be empty")
|
||||
}
|
||||
if !unicode.IsUpper(rune(name[0])) {
|
||||
panic("Component name must start with an uppercase letter")
|
||||
}
|
||||
client.RegisterComponent(name, renderFn)
|
||||
return func(props P) *vdom.VDomElem {
|
||||
return vdom.E(name, vdom.Props(props))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) RegisterComponent(name string, cfunc any) error {
|
||||
return c.Root.RegisterComponent(name, cfunc)
|
||||
}
|
||||
@ -185,6 +204,7 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
|
||||
Type: "backendupdate",
|
||||
Ts: time.Now().UnixMilli(),
|
||||
BlockId: c.RpcContext.BlockId,
|
||||
HasWork: len(c.Root.EffectWorkQueue) > 0,
|
||||
Opts: &c.Opts,
|
||||
RenderUpdates: []vdom.VDomRenderUpdate{
|
||||
{UpdateType: "root", VDom: *renderedVDom},
|
||||
|
@ -36,15 +36,15 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
|
||||
}
|
||||
// run events
|
||||
for _, event := range feUpdate.Events {
|
||||
if event.WaveId == "" {
|
||||
if event.GlobalEventType != "" {
|
||||
if impl.Client.GlobalEventHandler != nil {
|
||||
impl.Client.GlobalEventHandler(impl.Client, event)
|
||||
}
|
||||
} else {
|
||||
impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
|
||||
impl.Client.Root.Event(event.WaveId, event.EventType, event)
|
||||
}
|
||||
}
|
||||
if feUpdate.Resync {
|
||||
if feUpdate.Resync || true {
|
||||
return impl.Client.fullRender()
|
||||
}
|
||||
return impl.Client.incrementalRender()
|
||||
|
Loading…
Reference in New Issue
Block a user