From b685cdb3bbb12f9761568412eb728128866c8324 Mon Sep 17 00:00:00 2001 From: Rob Mayer <5171361+RobMayer@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:44:08 -0400 Subject: [PATCH] added fragment and render --- README.md | 67 ++++++++-- package.json | 2 +- src/index.ts | 343 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/jsx.d.ts | 303 --------------------------------------------- 4 files changed, 393 insertions(+), 322 deletions(-) delete mode 100644 src/jsx.d.ts diff --git a/README.md b/README.md index a50b360..dbd2a19 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ after: ```tsx const element = new UIElement(); -element.widget = ; +render(, element); refObject.addUI(element); ``` @@ -34,11 +34,12 @@ Add the "jsx" and "jsxFactory" fields to compilerOptions, and be sure to include ```json { "compilerOptions" { - ..., + /* ... other compilerOptions ... */, "jsx": "react", "jsxFactory": "jsxInTTPG", + "jsxFragmentFactory": "jsxFrag" }, - "include": [..., "./src/**/*.tsx"] + "include": [/* other includes */, "./src/**/*.tsx"] }, ``` @@ -47,6 +48,54 @@ additionally, you'll need to add this import at the top of any file that uses JS `import { jsxInTTPG } from "jsx-in-ttpg";` +## adding JSX to a UIElement or ScreenUIElement + +use the provider "render" function to add JSX to the UIElement/ScreenUIElement. The top-level JSX tag must be a vanilla widget (or a string or array string, since jsxInTTPG will wrap a lone string in a `` widget automagically). This could also be a custom component, so long as any nested components resolving to a top-level widget. + +```tsx +`import { render, jsxInTTPG, jsxFrag } from "jsx-in-ttpg";`; + +const ui = new UIElement(); +/* do stuff to set up ui element */ + +render(Hello There, ui); +``` + +## Fragments + +a custom component should always return a single JSXNode (or a primitive, null, etc, etc). If it turns out that you need to return multiple elements, wrap the returned elements in a fragment. You will need to import {jsxFrag} from 'jsx-in-ttpg' to do so; + +```tsx +`import { render, jsxInTTPG, jsxFrag } from "jsx-in-ttpg";`; + +const MyComponent = () => { + return ( + <> + + Hello + World + + + Some fancy [b]bolded[/b] text + + + ); +}; + +const ui = new UIElement(); + +render( + + + + + , + ui +); + +refObject.addUI(ui); +``` + # Syntax Every Tabletop Playground widget is resprestented in a JSX intrinsic element with certain attributes that function as function calls or property setters for that widget. @@ -496,7 +545,7 @@ const RobPanel = (props: { children?: SingleNode; title: TextNode; onClose?: () }; const element = new UIElement(); -element.widget = Hi There; +render(Hi There, element); refObject.addUI(element); ``` @@ -524,7 +573,7 @@ const checkRef = () => { } }; -element.widget = ( +render( @@ -534,7 +583,8 @@ element.widget = ( - + , + element ); refObject.addUI(element); @@ -552,7 +602,7 @@ const checkRef = () => { console.log(imageElement.getTintColor()); }; -element.widget = ( +render( {imageElement} @@ -560,7 +610,8 @@ element.widget = ( - + , + element ); refObject.addUI(element); diff --git a/package.json b/package.json index ea6435f..0498d5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jsx-in-ttpg", "license": "UNLICENSE", - "version": "1.0.0", + "version": "1.1.0", "scripts": { "build": "tsup src/index.ts --format cjs,esm --dts --no-splitting", "clean": "rm -rf ./dist", diff --git a/src/index.ts b/src/index.ts index 8e9d5ba..0966e14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ -import "./jsx.d"; - import { Text, Border, @@ -22,6 +20,12 @@ import { SelectionBox, Slider, TextBox, + HorizontalAlignment, + Player, + TextJustification, + VerticalAlignment, + UIElement, + ScreenUIElement, } from "@tabletop-playground/api"; type CanvasChild = { @@ -48,18 +52,30 @@ export const useRef = (initial: T | null = null): RefHandle return ref; }; +export const render = (widget: JSX.Element, element: UIElement | ScreenUIElement) => { + if (!(widget instanceof Widget)) { + throw Error("Top-level JSX.Element must be a widget"); + } + element.widget = widget; +}; + +export const asTextNode = (children: JSXNode): TextNode => { + if (!(children instanceof Widget)) { + return children; + } + return undefined; +}; + type ArrayOr = T | T[]; +export type JSXNode = JSX.Element; export type RefHandle = { current: T | null; clear: () => void }; export type RefObject = { current: T | null }; -export type SingleNode = Widget | ArrayOr; -export type MultiNode = ArrayOr; +type PossibleChildren = JSX.Element | ArrayOr | ArrayOr> | ArrayOr> | TextNode; export type TextNode = ArrayOr; -export type BoxNode = ArrayOr>; -export type CanvasNode = ArrayOr>; -export const boxChild = (weight: number, element: SingleNode): BoxChild => { +export const boxChild = (weight: number, element: JSX.Element): BoxChild => { return { tag: "boxchild", weight, @@ -67,7 +83,7 @@ export const boxChild = (weight: number, element: SingleNode): BoxChild => { +export const canvasChild = ({ x, y, width, height }: { x: number; y: number; width: number; height: number }, element: JSX.Element): CanvasChild => { return { tag: "canvaschild", x, @@ -121,8 +137,6 @@ const ensureWidgets = (...children: PossibleChildren[]): Widget[] => { }, []); }; -type PossibleChildren = SingleNode | MultiNode | TextNode | BoxNode | CanvasNode; - const ensureCanvasChildren = (...children: PossibleChildren[]): CanvasChild[] => { return children.reduce[]>((acc, child) => { if (child === null || child === undefined || typeof child === "boolean" || typeof child === "string" || typeof child === "number") { @@ -193,6 +207,13 @@ export const jsxInTTPG = (tag: ((props: any) => Widget) | keyof JSX.IntrinsicEle return element; }; +export const jsxFrag = (props?: { [key: string]: any }, ...children: PossibleChildren[]): PossibleChildren | PossibleChildren[] => { + if (children.length === 1) { + return children[0]; + } + return children; +}; + const createElement = (tag: T, attrs: { [key: string]: any }, children: PossibleChildren[]) => { switch (tag) { case "canvas": @@ -610,3 +631,305 @@ const INPUT_TYPES = { integer: 3, "positive-integer": 4, }; + +declare global { + namespace JSX { + type Element = Widget | ArrayOr; + interface ElementChildrenAttribute { + children: {}; + } + + interface IntrinsicElements { + image: { + ref?: { current: ImageWidget | null }; + disabled?: boolean; + hidden?: boolean; + onLoad?: (image: ImageWidget, filename: string, packageId: string) => void; + color?: Color | [number, number, number, number]; + width?: number; + height?: number; + children?: never; + } & ( + | { url: string } + | { + card: Card; + } + | { + src: string; + srcPackage?: string; + } + ); + imagebutton: { + ref?: { current: ImageButton | null }; + disabled?: boolean; + hidden?: boolean; + onLoad?: (image: ImageButton, filename: string, packageId: string) => void; + color?: Color | [number, number, number, number]; + width?: number; + height?: number; + onClick?: (image: ImageButton, player: Player) => void; + children?: never; + } & ( + | { url: string } + | { + card: Card; + } + | { + src: string; + srcPackage?: string; + } + ); + contentbutton: { + ref?: { current: ContentButton | null }; + disabled?: boolean; + hidden?: boolean; + onClick?: (image: ContentButton, player: Player) => void; + children?: JSX.Element; + }; + border: { + ref?: { current: Border | null }; + disabled?: boolean; + hidden?: boolean; + color?: Color | [number, number, number, number]; + children?: JSX.Element; + }; + canvas: { + ref?: { current: Canvas | null }; + disabled?: boolean; + hidden?: boolean; + children?: ArrayOr>; + }; + horizontalbox: { + ref?: { current: HorizontalBox | null }; + disabled?: boolean; + hidden?: boolean; + gap?: number; + valign?: VerticalAlignment; + halign?: HorizontalAlignment; + children?: ArrayOr>; + }; + verticalbox: { + ref?: { current: VerticalBox | null }; + disabled?: boolean; + hidden?: boolean; + gap?: number; + valign?: VerticalAlignment; + halign?: HorizontalAlignment; + children?: ArrayOr>; + }; + layout: { + ref?: { current: LayoutBox | null }; + disabled?: boolean; + hidden?: boolean; + valign?: VerticalAlignment; + halign?: HorizontalAlignment; + padding?: { + left?: number; + right?: number; + top?: number; + bottom?: number; + }; + maxHeight?: number; + minHeight?: number; + height?: number; + minWidth?: number; + maxWidth?: number; + width?: number; + children?: JSX.Element; + }; + text: { + ref?: { current: Text | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + wrap?: boolean; + justify?: TextJustification; + children?: TextNode; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + button: { + ref?: { current: Button | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + onClick?: (button: Button, player: Player) => void; + children?: TextNode; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + checkbox: { + ref?: { current: CheckBox | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + onChange?: (checkbox: CheckBox, player: Player | undefined, state: boolean) => void; + checked?: boolean; + label?: string | string[]; + children?: never; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + textarea: { + ref?: { current: MultilineTextBox | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + onChange?: (element: MultilineTextBox, player: Player | undefined, text: string) => void; + onCommit?: (element: MultilineTextBox, player: Player | undefined, text: string) => void; + maxLength?: number; + transparent?: boolean; + children?: TextNode; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + progressbar: { + ref?: { current: ProgressBar | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + wrap?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + value?: number; + label?: string | string[]; + children?: never; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + richtext: { + ref?: { current: RichText | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + wrap?: boolean; + justify?: TextJustification; + children?: TextNode; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + select: { + ref?: { current: SelectionBox | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + onChange?: (element: SelectionBox, player: Player | undefined, index: number, option: string) => void; + value?: string; + options: string[]; + children?: never; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + slider: { + ref?: { current: Slider | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + min?: number; + value?: number; + max?: number; + step?: number; + onChange?: (element: Slider, player: Player | undefined, value: number) => void; + inputWidth?: number; + children?: never; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + input: { + ref?: { current: TextBox | null }; + disabled?: boolean; + hidden?: boolean; + bold?: boolean; + italic?: boolean; + size?: number; + color?: Color | [number, number, number, number]; + onChange?: (element: TextBox, player: Player | undefined, text: string) => void; + onCommit?: (element: TextBox, player: Player | undefined, text: string, hardCommit: boolean) => void; + maxLength?: number; + transparent?: boolean; + selectOnFocus?: boolean; + value?: string; + type?: "string" | "float" | "positive-float" | "integer" | "positive-integer"; + children?: never; + } & ( + | { + font?: string; + } + | { + font: string; + fontPackage?: string; + } + ); + } + } +} diff --git a/src/jsx.d.ts b/src/jsx.d.ts deleted file mode 100644 index f14d745..0000000 --- a/src/jsx.d.ts +++ /dev/null @@ -1,303 +0,0 @@ -export {}; - -declare global { - namespace JSX { - type Element = import("@tabletop-playground/api").Widget; - interface ElementChildrenAttribute { - children: {}; - } - - interface IntrinsicElements { - image: { - ref?: { current: import("@tabletop-playground/api").ImageWidget | null }; - disabled?: boolean; - hidden?: boolean; - onLoad?: (image: import("@tabletop-playground/api").ImageWidget, filename: string, packageId: string) => void; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - width?: number; - height?: number; - children?: never; - } & ( - | { url: string } - | { - card: import("@tabletop-playground/api").Card; - } - | { - src: string; - srcPackage?: string; - } - ); - imagebutton: { - ref?: { current: import("@tabletop-playground/api").ImageButton | null }; - disabled?: boolean; - hidden?: boolean; - onLoad?: (image: import("@tabletop-playground/api").ImageButton, filename: string, packageId: string) => void; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - width?: number; - height?: number; - onClick?: (image: import("@tabletop-playground/api").ImageButton, player: import("@tabletop-playground/api").Player) => void; - children?: never; - } & ( - | { url: string } - | { - card: import("@tabletop-playground/api").Card; - } - | { - src: string; - srcPackage?: string; - } - ); - contentbutton: { - ref?: { current: import("@tabletop-playground/api").ContentButton | null }; - disabled?: boolean; - hidden?: boolean; - onClick?: (image: import("@tabletop-playground/api").ContentButton, player: import("@tabletop-playground/api").Player) => void; - children?: import(".").SingleNode; - }; - border: { - ref?: { current: import("@tabletop-playground/api").Border | null }; - disabled?: boolean; - hidden?: boolean; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - children?: import(".").SingleNode; - }; - canvas: { - ref?: { current: import("@tabletop-playground/api").Canvas | null }; - disabled?: boolean; - hidden?: boolean; - children?: import(".").CanvasNode; - }; - horizontalbox: { - ref?: { current: import("@tabletop-playground/api").HorizontalBox | null }; - disabled?: boolean; - hidden?: boolean; - gap?: number; - valign?: import("@tabletop-playground/api").VerticalAlignment; - halign?: import("@tabletop-playground/api").HorizontalAlignment; - children?: import(".").BoxNode; - }; - verticalbox: { - ref?: { current: import("@tabletop-playground/api").VerticalBox | null }; - disabled?: boolean; - hidden?: boolean; - gap?: number; - valign?: import("@tabletop-playground/api").VerticalAlignment; - halign?: import("@tabletop-playground/api").HorizontalAlignment; - children?: import(".").BoxNode; - }; - layout: { - ref?: { current: import("@tabletop-playground/api").LayoutBox | null }; - disabled?: boolean; - hidden?: boolean; - valign?: import("@tabletop-playground/api").VerticalAlignment; - halign?: import("@tabletop-playground/api").HorizontalAlignment; - padding?: { - left?: number; - right?: number; - top?: number; - bottom?: number; - }; - maxHeight?: number; - minHeight?: number; - height?: number; - minWidth?: number; - maxWidth?: number; - width?: number; - children?: import(".").SingleNode; - }; - text: { - ref?: { current: import("@tabletop-playground/api").Text | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - wrap?: boolean; - justify?: import("@tabletop-playground/api").TextJustification; - children?: import(".").TextNode; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - button: { - ref?: { current: import("@tabletop-playground/api").Button | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - onClick?: (button: import("@tabletop-playground/api").Button, player: import("@tabletop-playground/api").Player) => void; - children?: import(".").TextNode; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - checkbox: { - ref?: { current: import("@tabletop-playground/api").CheckBox | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - onChange?: (checkbox: import("@tabletop-playground/api").CheckBox, player: import("@tabletop-playground/api").Player | undefined, state: boolean) => void; - checked?: boolean; - label?: string | string[]; - children?: never; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - textarea: { - ref?: { current: import("@tabletop-playground/api").MultilineTextBox | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - onChange?: (element: import("@tabletop-playground/api").MultilineTextBox, player: import("@tabletop-playground/api").Player | undefined, text: string) => void; - onCommit?: (element: import("@tabletop-playground/api").MultilineTextBox, player: import("@tabletop-playground/api").Player | undefined, text: string) => void; - maxLength?: number; - transparent?: boolean; - children?: import(".").TextNode; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - progressbar: { - ref?: { current: import("@tabletop-playground/api").ProgressBar | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - wrap?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - value?: number; - label?: string | string[]; - children?: never; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - richtext: { - ref?: { current: import("@tabletop-playground/api").RichText | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - wrap?: boolean; - justify?: import("@tabletop-playground/api").TextJustification; - children?: import(".").TextNode; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - select: { - ref?: { current: import("@tabletop-playground/api").SelectionBox | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - onChange?: (element: import("@tabletop-playground/api").SelectionBox, player: import("@tabletop-playground/api").Player | undefined, index: number, option: string) => void; - value?: string; - options: string[]; - children?: never; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - slider: { - ref?: { current: import("@tabletop-playground/api").Slider | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - min?: number; - value?: number; - max?: number; - step?: number; - onChange?: (element: import("@tabletop-playground/api").Slider, player: import("@tabletop-playground/api").Player | undefined, value: number) => void; - inputWidth?: number; - children?: never; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - input: { - ref?: { current: import("@tabletop-playground/api").TextBox | null }; - disabled?: boolean; - hidden?: boolean; - bold?: boolean; - italic?: boolean; - size?: number; - color?: import("@tabletop-playground/api").Color | [number, number, number, number]; - onChange?: (element: import("@tabletop-playground/api").TextBox, player: import("@tabletop-playground/api").Player | undefined, text: string) => void; - onCommit?: (element: import("@tabletop-playground/api").TextBox, player: import("@tabletop-playground/api").Player | undefined, text: string, hardCommit: boolean) => void; - maxLength?: number; - transparent?: boolean; - selectOnFocus?: boolean; - value?: string; - type?: "string" | "float" | "positive-float" | "integer" | "positive-integer"; - children?: never; - } & ( - | { - font?: string; - } - | { - font: string; - fontPackage?: string; - } - ); - } - } -}