mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
vdom 5 (#1143)
This commit is contained in:
parent
ac6f9a05d4
commit
416c26c1cd
@ -159,7 +159,7 @@ tasks:
|
||||
vars:
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
- cp dist/bin/wsh-{{.VERSION}}-darwin.arm64 ~/.waveterm-dev/bin/wsh
|
||||
- cp dist/bin/wsh-{{.VERSION}}-darwin.arm64 ~/Library/Application\ Support/waveterm-dev/bin/wsh
|
||||
|
||||
build:wsh:internal:
|
||||
vars:
|
||||
|
@ -4,16 +4,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
var htmlCmdNewBlock bool
|
||||
var GlobalVDomClient *vdomclient.Client
|
||||
|
||||
func init() {
|
||||
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
|
||||
@ -27,16 +32,116 @@ 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)
|
||||
}
|
||||
|
||||
func BgItemTag(ctx context.Context, props map[string]any) 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
// 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})
|
||||
}
|
||||
|
||||
func MakeVDom() *vdom.VDomElem {
|
||||
vdomStr := `
|
||||
<div>
|
||||
<h1 style="color:red; background-color: #bind:$.bgcolor; border-radius: 4px; padding: 5px;">hello vdom world</h1>
|
||||
<div><bind key="$.text"/> | num[<bind key="$.num"/>]</div>
|
||||
<div className="root">
|
||||
<StyleTag/>
|
||||
<h1>Set Background</h1>
|
||||
<div>
|
||||
<button data-text="hello" onClick='#globalevent:clickinc'>increment</button>
|
||||
<wave:markdown text="*quick vdom application to set background colors*"/>
|
||||
</div>
|
||||
<div>
|
||||
<wave:markdown text="*hello from markdown*"/>
|
||||
<AllBgItemsTag/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@ -58,11 +163,15 @@ func htmlRun(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
GlobalVDomClient = client
|
||||
client.SetGlobalEventHandler(GlobalEventHandler)
|
||||
log.Printf("created client: %v\n", client)
|
||||
client.SetAtomVal("bgcolor", "#0000ff77")
|
||||
client.SetAtomVal("text", "initial text")
|
||||
client.SetAtomVal("num", 0)
|
||||
client.RegisterComponent("StyleTag", StyleTag)
|
||||
client.RegisterComponent("BgItemTag", BgItemTag)
|
||||
client.RegisterComponent("AllBgItemsTag", AllBgItemsTag)
|
||||
client.SetRootElem(MakeVDom())
|
||||
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
|
||||
if err != nil {
|
||||
|
@ -5,8 +5,8 @@ import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
|
||||
import { PlotView } from "@/app/view/plotview/plotview";
|
||||
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
|
||||
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom-view";
|
||||
import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom";
|
||||
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
|
||||
|
@ -8,7 +8,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
||||
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TermWshClient } from "@/app/view/term/term-wsh";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import {
|
||||
WOS,
|
||||
|
58
frontend/app/view/vdom/vdom-utils.tsx
Normal file
58
frontend/app/view/vdom/vdom-utils.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
||||
import type { CssNode, List, ListItem } from "css-tree";
|
||||
import * as csstree from "css-tree";
|
||||
|
||||
const TextTag = "#text";
|
||||
|
||||
// TODO support binding
|
||||
export function getTextChildren(elem: VDomElem): string {
|
||||
if (elem.tag == TextTag) {
|
||||
return elem.text;
|
||||
}
|
||||
if (!elem.children) {
|
||||
return null;
|
||||
}
|
||||
const textArr = elem.children.map((child) => {
|
||||
return getTextChildren(child);
|
||||
});
|
||||
return textArr.join("");
|
||||
}
|
||||
|
||||
export function convertVDomId(model: VDomModel, id: string): string {
|
||||
return model.blockId + "::" + id;
|
||||
}
|
||||
|
||||
export function validateAndWrapCss(model: VDomModel, cssText: string, wrapperClassName: string) {
|
||||
try {
|
||||
const ast = csstree.parse(cssText);
|
||||
csstree.walk(ast, {
|
||||
enter(node: CssNode, item: ListItem<CssNode>, list: List<CssNode>) {
|
||||
// Remove disallowed @rules
|
||||
const blockedRules = ["import", "font-face", "keyframes", "namespace", "supports"];
|
||||
if (node.type === "Atrule" && blockedRules.includes(node.name)) {
|
||||
list.remove(item);
|
||||
}
|
||||
// Remove :root selectors
|
||||
if (
|
||||
node.type === "Selector" &&
|
||||
node.children.some((child) => child.type === "PseudoClassSelector" && child.name === "root")
|
||||
) {
|
||||
list.remove(item);
|
||||
}
|
||||
|
||||
if (node.type === "IdSelector") {
|
||||
node.name = convertVDomId(model, node.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
const sanitizedCss = csstree.generate(ast);
|
||||
return `.${wrapperClassName} { ${sanitizedCss} }`;
|
||||
} catch (error) {
|
||||
// TODO better error handling
|
||||
console.error("CSS processing error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { VDomRoot } from "@/app/view/term/vdom";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import { useRef } from "react";
|
||||
|
||||
function makeVDomModel(blockId: string, nodeModel: NodeModel): VDomModel {
|
||||
return new VDomModel(blockId, nodeModel);
|
||||
}
|
||||
|
||||
type VDomViewProps = {
|
||||
model: VDomModel;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function VDomView({ blockId, model }: VDomViewProps) {
|
||||
let viewRef = useRef(null);
|
||||
model.viewRef = viewRef;
|
||||
return (
|
||||
<div className="vdom-view" ref={viewRef}>
|
||||
<VDomRoot model={model} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { makeVDomModel, VDomView };
|
5
frontend/app/view/vdom/vdom.less
Normal file
5
frontend/app/view/vdom/vdom.less
Normal file
@ -0,0 +1,5 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.view-vdom {
|
||||
}
|
@ -2,16 +2,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/app/element/markdown";
|
||||
import { VDomModel } from "@/app/view/term/vdom-model";
|
||||
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||
import clsx from "clsx";
|
||||
import debug from "debug";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
|
||||
import { convertVDomId, getTextChildren, validateAndWrapCss } from "@/app/view/vdom/vdom-utils";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
import "./vdom.less";
|
||||
|
||||
const TextTag = "#text";
|
||||
const FragmentTag = "#fragment";
|
||||
const WaveTextTag = "wave:text";
|
||||
const WaveNullTag = "wave:null";
|
||||
const StyleTagName = "style";
|
||||
|
||||
const VDomObjType_Ref = "ref";
|
||||
const VDomObjType_Binding = "binding";
|
||||
@ -25,7 +31,7 @@ const WaveTagMap: Record<string, VDomReactTagType> = {
|
||||
"wave:markdown": WaveMarkdown,
|
||||
};
|
||||
|
||||
const AllowedTags: { [tagName: string]: boolean } = {
|
||||
const AllowedSimpleTags: { [tagName: string]: boolean } = {
|
||||
div: true,
|
||||
b: true,
|
||||
i: true,
|
||||
@ -49,6 +55,30 @@ const AllowedTags: { [tagName: string]: boolean } = {
|
||||
select: true,
|
||||
option: true,
|
||||
form: true,
|
||||
label: true,
|
||||
table: true,
|
||||
thead: true,
|
||||
tbody: true,
|
||||
tr: true,
|
||||
th: true,
|
||||
td: true,
|
||||
hr: true,
|
||||
br: true,
|
||||
pre: true,
|
||||
code: true,
|
||||
};
|
||||
|
||||
const IdAttributes = {
|
||||
id: true,
|
||||
for: true,
|
||||
"aria-labelledby": true,
|
||||
"aria-describedby": true,
|
||||
"aria-controls": true,
|
||||
"aria-owns": true,
|
||||
form: true,
|
||||
headers: true,
|
||||
usemap: true,
|
||||
list: true,
|
||||
};
|
||||
|
||||
function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void {
|
||||
@ -165,6 +195,10 @@ function convertProps(elem: VDomElem, model: VDomModel): [GenericPropsType, Set<
|
||||
}
|
||||
// fallthrough to set props[key] = val
|
||||
}
|
||||
if (IdAttributes[key]) {
|
||||
props[key] = convertVDomId(model, val);
|
||||
continue;
|
||||
}
|
||||
props[key] = val;
|
||||
}
|
||||
return [props, atomKeys];
|
||||
@ -223,6 +257,20 @@ function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
);
|
||||
}
|
||||
|
||||
function StyleTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
const styleText = getTextChildren(elem);
|
||||
if (styleText == null) {
|
||||
return null;
|
||||
}
|
||||
const wrapperClassName = "vdom-" + model.blockId;
|
||||
// TODO handle errors
|
||||
const sanitizedCss = validateAndWrapCss(model, styleText, wrapperClassName);
|
||||
if (sanitizedCss == null) {
|
||||
return null;
|
||||
}
|
||||
return <style>{sanitizedCss}</style>;
|
||||
}
|
||||
|
||||
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
const props = useVDom(model, elem);
|
||||
if (elem.tag == WaveNullTag) {
|
||||
@ -235,7 +283,10 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
|
||||
if (waveTag) {
|
||||
return waveTag({ elem, model });
|
||||
}
|
||||
if (!AllowedTags[elem.tag]) {
|
||||
if (elem.tag == StyleTagName) {
|
||||
return <StyleTag elem={elem} model={model} />;
|
||||
}
|
||||
if (!AllowedSimpleTags[elem.tag]) {
|
||||
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
|
||||
}
|
||||
let childrenComps = convertChildren(elem, model);
|
||||
@ -280,4 +331,24 @@ function VDomRoot({ model }: { model: VDomModel }) {
|
||||
return <div className="vdom">{rtn}</div>;
|
||||
}
|
||||
|
||||
export { VDomRoot };
|
||||
function makeVDomModel(blockId: string, nodeModel: NodeModel): VDomModel {
|
||||
return new VDomModel(blockId, nodeModel);
|
||||
}
|
||||
|
||||
type VDomViewProps = {
|
||||
model: VDomModel;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function VDomView({ blockId, model }: VDomViewProps) {
|
||||
let viewRef = React.useRef(null);
|
||||
model.viewRef = viewRef;
|
||||
const vdomClass = "vdom-" + blockId;
|
||||
return (
|
||||
<div className={clsx("vdom-view", vdomClass)} ref={viewRef}>
|
||||
<VDomRoot model={model} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { makeVDomModel, VDomView };
|
@ -197,6 +197,10 @@ func makeNullVDom() *vdom.VDomElem {
|
||||
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
|
||||
}
|
||||
|
||||
func (c *Client) RegisterComponent(name string, cfunc vdom.CFunc) {
|
||||
c.Root.RegisterComponent(name, cfunc)
|
||||
}
|
||||
|
||||
func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
|
||||
c.Root.RunWork()
|
||||
c.Root.Render(c.RootElem)
|
||||
|
@ -270,11 +270,30 @@ func DBFindWindowForTabId(ctx context.Context, tabId string) (string, error) {
|
||||
|
||||
func DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) {
|
||||
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
|
||||
query := `
|
||||
SELECT t.oid
|
||||
FROM db_tab t, json_each(data->'blockids') je
|
||||
WHERE je.value = ?;`
|
||||
return tx.GetString(query, blockId), nil
|
||||
iterNum := 1
|
||||
for {
|
||||
if iterNum > 5 {
|
||||
return "", fmt.Errorf("too many iterations looking for tab in block parents")
|
||||
}
|
||||
query := `
|
||||
SELECT json_extract(b.data, '$.parentoref') AS parentoref
|
||||
FROM db_block b
|
||||
WHERE b.oid = ?;`
|
||||
parentORef := tx.GetString(query, blockId)
|
||||
oref, err := waveobj.ParseORef(parentORef)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bad block parent oref: %v", err)
|
||||
}
|
||||
if oref.OType == "tab" {
|
||||
return oref.OID, nil
|
||||
}
|
||||
if oref.OType == "block" {
|
||||
blockId = oref.OID
|
||||
iterNum++
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("bad parent oref type: %v", oref.OType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user