`;
7 | }
8 | if (typeof obj === 'boolean' || typeof obj === 'number') {
9 | return `${obj}`;
10 | }
11 | if (typeof obj === 'undefined' || !obj) {
12 | return 'nil';
13 | }
14 | if (depth < 0) {
15 | return '{ ... }';
16 | }
17 | const inner = Object.keys(obj)
18 | .map(key => `${key}: ${stringify(obj[key], depth - 1)}`)
19 | .join(', ');
20 | return `{ ${inner} }`;
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 | "target": "ESNext",
5 | "module": "commonjs",
6 | "lib": [
7 | "ESNext",
8 | "DOM"
9 | ],
10 | "jsx": "react",
11 | "jsxFactory": "createElement",
12 | "declaration": true,
13 | "strict": true,
14 | "rootDir": "./src/",
15 | "outDir": "./dist/",
16 | "moduleResolution": "node",
17 | "skipLibCheck": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "typeRoots": [
20 | "node_modules/@wartoshika/wow-declarations",
21 | "node_modules/lua-types/5.1",
22 | "node_modules/@types"
23 | ],
24 | "types": [
25 | "lua-types/5.1",
26 | "@wartoshika/wow-declarations"
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # React for WoW Addons
2 |
3 | This library provides a react-style rendering engine for the development of UI components for World of Warcraft. Using JSX, one can define custom components which render to World of Warcraft's UI. Gain all the advantages of React-style UI definition for your World of Warcraft addon, complete with full type hints, and differentiable rendering.
4 |
5 | This project is compiled down to Lua using typescript-to-lua [GitHub](https://github.com/TypeScriptToLua/TypeScriptToLua).
6 |
7 | This project is based off of work done by Tim Stirrat in [tsCooldown](https://github.com/tstirrat/tsCoolDown).
8 |
9 | # Usage
10 |
11 | TODO
12 |
13 | # Note
14 | This library is largely blocked on the merge of https://github.com/TypeScriptToLua/TypeScriptToLua/pull/909, which will support importing/requiring lua code from dependencies. Once that PR is merged, this package should be largely ready-to-use
15 |
--------------------------------------------------------------------------------
/src/component.ts:
--------------------------------------------------------------------------------
1 | import { InternalElement } from './element';
2 | import { Instance, reconcile } from './reconciler';
3 |
4 | export class Component {
5 | public state: S = {} as any;
6 | constructor(public props: P = {} as any) { }
7 |
8 | private __internalInstance!: Instance;
9 |
10 | setState(partialState: Partial) {
11 | this.state = { ...this.state, ...partialState };
12 | updateInstance(this.__internalInstance);
13 | }
14 |
15 | render(): InternalElement | null {
16 | throw 'render not implemented';
17 | }
18 | }
19 |
20 | function updateInstance(internalInstance: Instance) {
21 | const parentDom = internalInstance.hostFrame.GetParent() as WoWAPI.Frame;
22 | const element = internalInstance.element;
23 | if (parentDom) {
24 | reconcile(parentDom, internalInstance, element);
25 | } else {
26 | throw 'Tried to reconcile instance with no dom.parentDom';
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@brusalk/react-wow-addon",
3 | "version": "1.0.3",
4 | "description": "React-style UI Framework for World of Warcraft AddOns",
5 | "main": "./dist/index.js",
6 | "types": "./dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "prepublish": "npm run build",
10 | "test": "echo NYI"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "github:Brusalk/react-wow-addon"
15 | },
16 | "keywords": [
17 | "World of Warcraft",
18 | "WoW",
19 | "React",
20 | "DOM",
21 | "UI",
22 | "tstl",
23 | "typescript",
24 | "typescript-to-lua"
25 | ],
26 | "author": "Brusalk",
27 | "license": "MIT",
28 | "devDependencies": {
29 | "tslint": "^5.20.0"
30 | },
31 | "dependencies": {
32 | "@wartoshika/wow-declarations": "^8.3.0-release.1",
33 | "lua-types": "^2.8.0",
34 | "ts-node": "^9.0.0",
35 | "typescript-to-lua": "^0.35.0",
36 | "typescript": "^3.6.3"
37 | },
38 | "files": [
39 | "dist/",
40 | "@types"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Brusalk, Tim Stirrat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/element.ts:
--------------------------------------------------------------------------------
1 | /** @noSelfInFile */
2 |
3 | import { Component } from '.';
4 |
5 | export interface InternalElement {
6 | type: string | Component;
7 | props: Props;
8 | }
9 |
10 | export type RawChild =
11 | InternalElement | string | boolean | null;
12 |
13 | export type RenderableChildElement = InternalElement | string;
14 |
15 | interface Props {
16 | key?: string;
17 | children?: InternalElement[];
18 | nodeValue?: string;
19 | [k: string]: any;
20 | }
21 |
22 | export const TEXT_ELEMENT = 'TEXT ELEMENT';
23 |
24 | export function createElement(
25 | type: string | Component, config: Props, rawChildren?: RawChild[][]) {
26 | const props: Props = { ...config };
27 | const flattenedChildren = rawChildren && rawChildren.length ? rawChildren.flat() : [];
28 | props.children =
29 | flattenedChildren
30 | .filter((c): c is RenderableChildElement =>
31 | c != null && typeof c !== 'boolean' &&
32 | // filters out empty objects which are left because Array.flat() is not correct
33 | (typeof c !== 'string' && !!c.type))
34 | .map(c => typeof c === 'string' ? createTextElement(c) : c);
35 |
36 | // print('createElement', typeof type === 'string' ? type : 'Component', stringify(props, 1));
37 | // print('createElement .children', typeof type === 'string' ? type : 'Component', stringify(props.children, 1));
38 | return { type, props };
39 | }
40 |
41 | function createTextElement(value: string): InternalElement {
42 | return createElement(TEXT_ELEMENT, { nodeValue: value });
43 | }
44 |
--------------------------------------------------------------------------------
/src/ts_transformers/RequirePreload.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-object-literal-type-assertion */
2 | import * as ts from "typescript";
3 | import {
4 | Block,
5 | createBlock,
6 | createCallExpression,
7 | createExpressionStatement,
8 | createFunctionExpression,
9 | createIdentifier,
10 | createStringLiteral,
11 | Plugin
12 | } from "typescript-to-lua";
13 |
14 | export const RequirePreload: Plugin = {
15 | visitors: {
16 | [ts.SyntaxKind.SourceFile]: (node, context) => {
17 | const [fileContent] = context.superTransformNode(node) as Block[];
18 | if (context.isModule) {
19 | const moduleFunction = createFunctionExpression(
20 | fileContent,
21 | undefined,
22 | undefined,
23 | undefined
24 | );
25 |
26 | let moduleName = context.sourceFile.fileName.split("src")[1];
27 | if (moduleName.startsWith("/")) moduleName = moduleName.substring(1);
28 | if (moduleName.endsWith(".tsx")) moduleName = moduleName.substring(0, moduleName.length - 4);
29 | if (moduleName.endsWith(".ts")) moduleName = moduleName.substring(0, moduleName.length - 3);
30 | moduleName = moduleName.split("/").join(".");
31 | moduleName = moduleName.replace(".index", "");
32 | // Skip init.lua so it can be the entry-point
33 | if (moduleName === "init") return fileContent;
34 |
35 | // Generates:
36 | // tstl_register_module("module/name", function() ... end)
37 | const moduleCallExpression = createCallExpression(
38 | createIdentifier("tstl_register_module"),
39 | [createStringLiteral(moduleName), moduleFunction]
40 | );
41 |
42 | return createBlock([createExpressionStatement(moduleCallExpression)]);
43 | }
44 | return fileContent;
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/@types/jsx.d.ts:
--------------------------------------------------------------------------------
1 | /** @noSelfInFile */
2 |
3 | declare namespace JSX {
4 | interface PointDefinition {
5 | point: WoWAPI.Point;
6 | relativePoint?: WoWAPI.Point;
7 | relativeFrame?: WoWAPI.Region | string;
8 | x?: number;
9 | y?: number;
10 | }
11 |
12 | type Point = PointDefinition | WoWAPI.Point;
13 |
14 | type Color4 = [number, number, number, number];
15 | type Tuple = [T, T];
16 | type Size = Tuple;
17 | type Font = [string, number];
18 |
19 | interface BaseProps {
20 | key?: string;
21 | children?: any;
22 | }
23 |
24 | interface BaseFrameProps extends BaseProps {
25 | name?: string;
26 | inheritsFrom?: string;
27 | Width?: number;
28 | Height?: number;
29 | Size?: Size;
30 | Points?: Point[];
31 | Point?: Point;
32 | Backdrop?: WoWAPI.Backdrop;
33 | BackdropBorderColor?: Color4;
34 | BackdropColor?: Color4;
35 |
36 | OnUpdate?: (this: void, frame: WoWAPI.Frame, secondsElapsed: number) => void;
37 |
38 | Clickable?: WoWAPI.MouseButton[];
39 | OnClick?: (this: void, frame: WoWAPI.Frame, button: WoWAPI.MouseButton, down: boolean) => void;
40 |
41 | Draggable?: WoWAPI.MouseButton[];
42 | Movable?: boolean;
43 | OnDragStart?: (this: void, frame: WoWAPI.Frame, button: WoWAPI.MouseButton, down: boolean) => void;
44 | OnDragStop?: (this: void, frame: WoWAPI.Frame, button: WoWAPI.MouseButton, down: boolean) => void;
45 | }
46 |
47 | interface LayeredRegionProps extends BaseFrameProps {
48 | VertexColor?: Color4;
49 | DrawLayer?: WoWAPI.Layer | [WoWAPI.Layer, number];
50 | }
51 |
52 | interface StatusBarProps extends BaseFrameProps {
53 | MinMaxValues?: Tuple;
54 | Value?: number;
55 | StatusBarTexture?: string;
56 | StatusBarColor?: Color4;
57 | }
58 |
59 | interface TextureProps extends LayeredRegionProps {
60 | Texture?: WoWAPI.TexturePath;
61 | }
62 |
63 | interface FontInstanceProps extends LayeredRegionProps {
64 | Font?: Font;
65 | }
66 |
67 | interface FontStringProps extends FontInstanceProps {
68 | Text?: string;
69 | JustifyH?: WoWAPI.HorizontalAlign;
70 | JustifyV?: WoWAPI.VerticalAlign;
71 | TextColor?: Color4;
72 | }
73 |
74 | interface IntrinsicElements {
75 | button: BaseFrameProps;
76 | 'color-select': BaseFrameProps;
77 | cooldown: BaseFrameProps;
78 | 'edit-box': BaseFrameProps;
79 | frame: BaseFrameProps;
80 | 'game-tooltip': BaseFrameProps;
81 | 'message-frame': BaseFrameProps;
82 | minimap: BaseFrameProps;
83 | model: BaseFrameProps;
84 | 'scroll-frame': BaseFrameProps;
85 | 'scrolling-message-frame': BaseFrameProps;
86 | 'simple-html': BaseFrameProps;
87 | slider: StatusBarProps;
88 | 'status-bar': StatusBarProps;
89 |
90 | // Other things
91 | 'font-string': FontStringProps;
92 | texture: TextureProps;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by filing an issue. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/ts_transformers/WowJsxTransformer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-object-literal-type-assertion */
2 | import * as ts from "typescript";
3 | import {
4 | createCallExpression,
5 | createIdentifier,
6 | createNilLiteral,
7 | createStringLiteral,
8 | createTableIndexExpression,
9 | FunctionVisitor,
10 | Plugin,
11 | TransformationContext,
12 | VisitorResult
13 | } from "typescript-to-lua";
14 | import { literalVisitors } from "typescript-to-lua/dist/transformation/visitors/literal";
15 |
16 | const transformObjectLiteral = literalVisitors[
17 | ts.SyntaxKind.ObjectLiteralExpression
18 | ] as FunctionVisitor;
19 | const transformArrayLiteral = literalVisitors[
20 | ts.SyntaxKind.ArrayLiteralExpression
21 | ] as FunctionVisitor;
22 |
23 | function transformJsxAttributesExpression(
24 | expression: ts.JsxAttributes,
25 | context: TransformationContext
26 | ): VisitorResult {
27 | if (
28 | expression.properties.find(
29 | element => element.kind === ts.SyntaxKind.JsxSpreadAttribute
30 | )
31 | ) {
32 | throw new Error("Unsupported: JsxSpreadAttribute");
33 | }
34 | const properties = expression.properties
35 | .filter(
36 | (element): element is ts.JsxAttribute =>
37 | element.kind !== ts.SyntaxKind.JsxSpreadAttribute
38 | )
39 | .map(element => {
40 | const valueOrExpression = element.initializer
41 | ? element.initializer
42 | : ts.createLiteral(true);
43 | return ts.createPropertyAssignment(element.name, valueOrExpression);
44 | });
45 |
46 | return transformObjectLiteral(ts.createObjectLiteral(properties), context);
47 | }
48 | function transformJsxOpeningElement(
49 | expression: ts.JsxSelfClosingElement | ts.JsxOpeningElement,
50 | context: TransformationContext,
51 | children?: ts.NodeArray
52 | ): VisitorResult {
53 | //
54 | // React.createElement(Something, {a = 'b'})
55 | const [library, create] = context.options.jsxFactory
56 | ? context.options.jsxFactory.split(".")
57 | : ["React", "createElement"];
58 | const createElement = createTableIndexExpression(
59 | createIdentifier(library),
60 | createStringLiteral(create)
61 | );
62 | const tagName = expression.tagName.getText();
63 |
64 | const tag =
65 | tagName.toLowerCase() === tagName
66 | ? createStringLiteral(tagName)
67 | : createIdentifier(tagName);
68 |
69 | const props = transformJsxAttributesExpression(
70 | expression.attributes,
71 | context
72 | );
73 |
74 | if (children) {
75 | const childrenOrStringLiterals = children
76 | .filter(child => !ts.isJsxText(child) || child.text.trim() !== "")
77 | .map(child =>
78 | ts.isJsxText(child) ? ts.createStringLiteral(child.text.trim()) : child
79 | );
80 | const arrayLiteral = ts.createArrayLiteral(childrenOrStringLiterals, true);
81 |
82 | return createCallExpression(
83 | createElement,
84 | [tag, props, transformArrayLiteral(arrayLiteral, context)],
85 | expression
86 | );
87 | }
88 |
89 | return createCallExpression(createElement, [tag, props], expression);
90 | }
91 |
92 | function transformJsxElement(
93 | expression: ts.JsxElement | ts.JsxSelfClosingElement,
94 | context: TransformationContext
95 | ): VisitorResult {
96 | if (ts.isJsxSelfClosingElement(expression)) {
97 | return transformJsxOpeningElement(expression, context);
98 | }
99 | return transformJsxOpeningElement(
100 | expression.openingElement,
101 | context,
102 | expression.children
103 | );
104 | }
105 |
106 | export const WowJsxTransformer: Plugin = {
107 | visitors: {
108 | [ts.SyntaxKind.JsxSelfClosingElement]: transformJsxElement,
109 | [ts.SyntaxKind.JsxElement]: transformJsxElement,
110 | [ts.SyntaxKind.JsxExpression]: (node, context) => {
111 | if (node.expression) {
112 | return context.transformExpression(node.expression);
113 | }
114 | return createNilLiteral();
115 | },
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/reconciler.ts:
--------------------------------------------------------------------------------
1 | import { Component } from './component';
2 | import { InternalElement, TEXT_ELEMENT } from './element';
3 | import { cleanupFrame, createFrame, updateFrameProperties } from './wow-utils';
4 |
5 | export interface Instance {
6 | publicInstance?: Component;
7 | childInstance: Instance | null;
8 | childInstances: Array;
9 | hostFrame: WoWAPI.Region;
10 | element: InternalElement;
11 | }
12 |
13 | let rootInstance: Instance | null = null;
14 |
15 | export function render(element: InternalElement, container: WoWAPI.Region) {
16 | const prevInstance = rootInstance;
17 | const nextInstance = reconcile(container, prevInstance, element);
18 | rootInstance = nextInstance;
19 | }
20 |
21 | export function reconcile(
22 | parentFrame: WoWAPI.Region, instance: Instance | null,
23 | element: InternalElement | null): Instance | null {
24 | if (!instance) {
25 | // Create instance
26 | assert(element, 'element should not be null')
27 | return instantiate(element!, parentFrame);
28 | } else if (!element) {
29 | // Remove instance
30 | cleanupFrames(instance);
31 | return null;
32 | } else if (instance.element.type !== element.type) {
33 | // Replace instance
34 | const newInstance = instantiate(element, parentFrame);
35 | cleanupFrames(instance);
36 | return newInstance;
37 | } else if (typeof element.type === 'string') {
38 | // Update host element
39 | updateFrameProperties(
40 | instance.hostFrame, instance.element.props, element.props);
41 | instance.childInstances = reconcileChildren(instance, element);
42 | instance.element = element;
43 | return instance;
44 | } else if (instance.publicInstance) {
45 | // print('reconcile composite', (element.type as any).name, stringify(element.props));
46 | // Update composite instance
47 | instance.publicInstance.props = element.props;
48 | const childElement = instance.publicInstance.render();
49 | const oldChildInstance = instance.childInstance;
50 | const childInstance =
51 | reconcile(parentFrame, oldChildInstance, childElement);
52 |
53 | if (!childInstance) {
54 | throw 'Failed to update composite instance';
55 | }
56 |
57 | instance.hostFrame = childInstance.hostFrame;
58 | instance.childInstance = childInstance;
59 | instance.element = element;
60 | return instance;
61 | } else {
62 | throw 'Reconciler catch all error';
63 | }
64 | }
65 |
66 | function cleanupFrames(instance: Instance) {
67 | // TODO: composite objects need special cleanup, this should be part of reconcile
68 | if (instance.childInstances) {
69 | instance.childInstances.forEach(child => child && cleanupFrames(child));
70 | }
71 | if (instance.childInstance) {
72 | cleanupFrames(instance.childInstance);
73 | }
74 | cleanupFrame(instance.hostFrame);
75 | }
76 |
77 | function reconcileChildren(instance: Instance, element: InternalElement) {
78 | const hostFrame = instance.hostFrame;
79 | const childInstances = instance.childInstances;
80 | const nextChildElements = element.props.children || [];
81 | const newChildInstances = [];
82 | const count = Math.max(childInstances.length, nextChildElements.length);
83 | for (let i = 0; i < count; i++) {
84 | const childInstance = childInstances[i];
85 | const childElement = nextChildElements[i];
86 | const newChildInstance = reconcile(hostFrame, childInstance, childElement);
87 | newChildInstances.push(newChildInstance);
88 | }
89 | return newChildInstances.filter(instance => instance != null);
90 | }
91 |
92 | function instantiate(
93 | element: InternalElement, parentFrame: WoWAPI.Region): Instance {
94 | const { type, props } = element;
95 |
96 | if (typeof type === 'string') {
97 | if (type === TEXT_ELEMENT) {
98 | throw 'Cannot create inline text, yet';
99 | }
100 | // print('instantiate', type, stringify(props));
101 |
102 | // Instantiate host element
103 | const frame = createFrame(type, parentFrame, props);
104 |
105 | updateFrameProperties(frame, {}, props);
106 |
107 | const childElements = props.children || [];
108 | const childInstances =
109 | childElements.map(child => instantiate(child, frame));
110 |
111 | const instance: Instance =
112 | { hostFrame: frame, element, childInstances, childInstance: null };
113 | return instance;
114 |
115 | } else {
116 | // print('instantiate', (type as any).name, stringify(props));
117 | // Instantiate component element
118 | const instance = {} as Instance;
119 | const publicInstance = createPublicInstance(element, instance);
120 | const childElement = publicInstance.render();
121 | const childInstance = instantiate(childElement, parentFrame);
122 | const hostFrame = childInstance.hostFrame;
123 |
124 | const updateProps:
125 | Partial = { hostFrame, element, childInstance, publicInstance };
126 | Object.assign(instance, updateProps);
127 | return instance;
128 | }
129 | }
130 |
131 | function createPublicInstance(
132 | element: InternalElement, internalInstance: Instance) {
133 | const { type: ComponentType, props } = element;
134 | if (!ComponentType) {
135 | throw 'Tried createPublicInstance() with undefined';
136 | }
137 |
138 | if (typeof ComponentType === 'string') {
139 | throw 'Tried createPublicInstance() with string';
140 | }
141 |
142 | const publicInstance = new (ComponentType as any)(props);
143 | publicInstance.__internalInstance = internalInstance;
144 | return publicInstance;
145 | }
146 |
--------------------------------------------------------------------------------
/src/wow-utils.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | type Props = Record;
4 |
5 | const frameCache: Record = {
6 | };
7 |
8 | function getCache(type: string): WoWAPI.Region | undefined {
9 | // if (frameCache[type]) {
10 | // return frameCache[type].length ? frameCache[type].pop() : undefined;
11 | // }
12 | return undefined;
13 | }
14 |
15 | function setCache(frame: WoWAPI.Region) {
16 | const type = frame.GetObjectType();
17 | print('store cache', type);
18 | if (frameCache[type] && frameCache[type].length) {
19 | frameCache[type].push(frame);
20 | } else {
21 | frameCache[type] = [frame];
22 | }
23 | }
24 |
25 | export function createFrame(
26 | jsxType: string, parentFrame: WoWAPI.Region,
27 | props: Props): WoWAPI.Region {
28 | const frameType = pascalCase(jsxType);
29 |
30 | let frame = getCache(frameType);
31 |
32 | if (frame) {
33 | print('got frame from cache', frameType, frame, 'parent:', parentFrame);
34 | frame.SetParent(parentFrame);
35 | frame.Show();
36 | return frame;
37 | }
38 |
39 | if (frameType === 'FontString') {
40 | frame = (parentFrame as WoWAPI.Frame)
41 | .CreateFontString(props.name, props.DrawLayer || 'ARTWORK', props.inheritsFrom);
42 | } else if (frameType === 'Texture') {
43 | frame = (parentFrame as WoWAPI.Frame)
44 | .CreateTexture(props.name, props.DrawLayer || 'ARTWORK', props.inheritsFrom);
45 | } else {
46 | frame =
47 | CreateFrame(frameType as WoWAPI.FrameType, props.name, parentFrame, props.inheritsFrom || "BackdropTemplate") as
48 | WoWAPI.Frame;
49 | }
50 | // frame.SetParent(parentFrame);
51 | print('created frame:', frameType);
52 | return frame;
53 | }
54 |
55 | export function cleanupFrame(frame: WoWAPI.Region) {
56 | print('cleaning up frame', frame.GetObjectType(), frame);
57 | frame.Hide();
58 | frame.ClearAllPoints();
59 | if (frame.GetObjectType() as string === 'Texture' ||
60 | frame.GetObjectType() as string === 'FontString') {
61 | frame.SetParent(UIParent);
62 | } else {
63 | frame.SetParent(null);
64 | }
65 | setCache(frame);
66 | }
67 |
68 | const isEvent = (name: string) => name.startsWith('On');
69 | const isStandardProperty = (name: string) => !isEvent(name) &&
70 | !isOrderedProperty(name) && name !== 'children' && name !== 'Points' && name !== 'Point' &&
71 | name !== 'name' && name !== 'DrawLayer' && name !== 'inheritsFrom' && name !== 'Clickable' &&
72 | name !== 'Draggable';
73 |
74 | /**
75 | * These properties must be set _before_ their other properties e.g. Background
76 | * must be set before BackgroundColor
77 | */
78 | const isOrderedProperty = (name: string) => name === 'Font' ||
79 | name === 'Background' || name === 'Texture' || name === 'Backdrop';
80 | /**
81 | * These properties take table values, which should be set verbatim. Array
82 | * values will apply each item as an argument to SetX. These values should not
83 | * be interpreted as arrays.
84 | */
85 | const isTableValue = (name: string) => name === 'Backdrop';
86 |
87 | export function updateFrameProperties(
88 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) {
89 | updateFramePoints(frame, nextProps);
90 | updateFrameLayer(frame, nextProps);
91 | updateFrameEvents(frame, prevProps, nextProps);
92 | updateOrderSpecificProperties(frame, prevProps, nextProps);
93 | updateRemainingProperties(frame, prevProps, nextProps);
94 | }
95 |
96 | function updateOrderSpecificProperties(
97 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) {
98 | // Remove properties that are no longer specified
99 | Object.keys(prevProps)
100 | .filter(key => isOrderedProperty(key) && !nextProps[key])
101 | .forEach(key => {
102 | attemptSetProperty(frame, key, null);
103 | });
104 | // Set properties
105 | Object.keys(nextProps).filter(isOrderedProperty).forEach(key => {
106 | attemptSetProperty(frame, key, nextProps[key]);
107 | });
108 | }
109 |
110 | function updateRemainingProperties(
111 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) {
112 | // Remove properties that are no longer specified
113 | Object.keys(prevProps)
114 | .filter(key => isStandardProperty(key) && !nextProps[key])
115 | .forEach(key => {
116 | attemptSetProperty(frame, key, null);
117 | });
118 | // Set properties
119 | Object.keys(nextProps).filter(isStandardProperty).forEach(key => {
120 | attemptSetProperty(frame, key, nextProps[key]);
121 | });
122 | }
123 |
124 | function updateFrameEvents(
125 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) {
126 | // Detach removed event listeners
127 | Object.keys(prevProps)
128 | .filter(key => isEvent(key) && !nextProps[key])
129 | .forEach(event => {
130 | (frame as WoWAPI.Frame).SetScript(event as WoWAPI.Event.OnAny, undefined);
131 | });
132 |
133 | if (nextProps['Clickable']) {
134 | (frame as any).RegisterForClicks('RightButton');
135 | }
136 | if (nextProps['Draggable']) {
137 | (frame as any).RegisterForDrag(...nextProps['Draggable']);
138 | }
139 |
140 | // Add new event listeners
141 | Object.keys(nextProps)
142 | .filter(key => isEvent(key) && prevProps[key] !== nextProps[key])
143 | .forEach(event => {
144 | // print('attaching event', event);
145 | (frame as WoWAPI.Frame).SetScript(event as WoWAPI.Event.OnAny, nextProps[event]);
146 | });
147 | }
148 |
149 | /** Handle frame points, size to parent unless specified. */
150 | function updateFramePoints(frame: WoWAPI.Region, nextProps: JSX.BaseFrameProps) {
151 | frame.ClearAllPoints();
152 | if (nextProps.Point) {
153 | setPoint(frame, nextProps.Point);
154 | return;
155 | }
156 | if (nextProps.Points) {
157 | const points = nextProps.Points;
158 | points.forEach(pointDef => setPoint(frame, pointDef));
159 | } else {
160 | // Fill to parent
161 | frame.SetAllPoints();
162 | }
163 | }
164 |
165 | /** Handle frame points, size to parent unless specified. */
166 | function updateFrameLayer(frame: WoWAPI.Region, nextProps: JSX.LayeredRegionProps) {
167 | const region = frame as WoWAPI.LayeredRegion;
168 | const layer = nextProps.DrawLayer;
169 |
170 | if (!layer || typeof region.SetDrawLayer !== 'function') {
171 | return;
172 | }
173 |
174 | if (typeof layer === 'string') {
175 | region.SetDrawLayer(layer, 0);
176 | return;
177 | }
178 |
179 | region.SetDrawLayer(layer[0] as WoWAPI.Layer, layer[1]);
180 | }
181 |
182 | /** Create a point declaration */
183 | export function P(
184 | point: WoWAPI.Point, x?: number, y?: number, relativePoint?: WoWAPI.Point,
185 | relativeFrame?: WoWAPI.Region): JSX.PointDefinition {
186 | // TODO: memoize for perf
187 | return { point, relativePoint, relativeFrame, x, y };
188 | }
189 |
190 | function setPoint(frame: WoWAPI.Region, pointDef: JSX.Point) {
191 | if (typeof pointDef === 'string') {
192 | frame.SetPoint(pointDef);
193 | } else {
194 | const { point, relativePoint, relativeFrame, x, y } = pointDef;
195 | const relativeTo = relativePoint || point;
196 | // print('setPoint', Object.keys(pointDef).join(', '));
197 | if (relativeFrame) {
198 | frame.SetPoint(point, relativeFrame, relativeTo, x || 0, y || 0);
199 | } else {
200 | const parent = frame.GetParent() as WoWAPI.Region;
201 | frame.SetPoint(point, parent, relativeTo, x || 0, y || 0);
202 | }
203 | }
204 | }
205 |
206 | function attemptSetProperty(frame: WoWAPI.Region, key: string, value: any) {
207 | const region = frame as any as Record void>;
208 | const setter = `Set${key}`;
209 | const setterFn = region[setter];
210 | assert(setterFn, `Tried to use ${setter} and it did not exist on ${region}`);
211 |
212 | if (setterFn && typeof setterFn == 'function') {
213 | if (typeof value === 'string' || typeof value === 'number' ||
214 | typeof value === 'boolean' || isTableValue(key)) {
215 | region[setter](value);
216 | } else {
217 | // print( `calling ${setter} with array elements as args:`,
218 | // (value as any[]).join(', '));
219 | setterFn.apply(region, value);
220 | }
221 | }
222 | }
223 |
224 | export function sentenceCase(str: string) {
225 | return str[0].toUpperCase() + str.slice(1).toLowerCase();
226 | }
227 |
228 | export function pascalCase(kebabCase: string) {
229 | return kebabCase.split('-').map(sentenceCase).join('');
230 | }
231 |
--------------------------------------------------------------------------------