├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── src ├── ReactInstanceHandles.js ├── ReactUMGComponent.js ├── ReactUMGDefaultInjection.js ├── ReactUMGEmptyComponent.js ├── ReactUMGMount.js ├── ReactUMGNodeMap.js ├── ReactUMGReconcileTransaction.js ├── UMGRoots.js ├── components ├── ReactUMGClassMap.js ├── index.js └── set_attrs.js ├── devtools ├── InitializeJavaScriptAppEngine.js └── setupDevtools.js ├── editor-maker.js └── index.js /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | registry-url: https://registry.npmjs.org/ 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | React-UMG 2 | ________________________________________ 3 | Copyright (C) 2016 NCSOFT Corporation and other contributors. All Rights Reserved. 4 | Copyright (C) 2016 drywolf. All Rights Reserved. 5 | Copyright (C) 2016 Facebook Inc. All Rights Reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | Permission is granted to anyone to use this software subject to acceptance and compliance with the acceptance and compliance with any other open source licenses attached below this copyright notice. 10 | The following restrictions shall also apply: 11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | 3. Neither the name of NCSOFT Corporation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 | ________________________________________ 17 | This software uses Open Source Software (OSS). You can find the link for the source code of these open source projects, along with applicable license information, below. 18 | 19 | react-umg 20 | https://github.com/drywolf/react-umg 21 | Copyright (C) 2016 drywolf 22 | MIT License 23 | See license text at https://github.com/drywolf/react-umg/blob/master/LICENSE 24 | 25 | React Native 26 | https://github.com/facebook/react-native 27 | Copyright (C) 2016 Facebook Inc. 28 | BSD-3-Clause License 29 | See license text at https://github.com/facebook/react-native/blob/master/LICENSE 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [React-UMG](https://github.com/ncsoft/React-UMG) · [![npm version](https://img.shields.io/npm/v/react-umg.svg?style=flat)](https://www.npmjs.com/package/react-umg) 2 | 3 | This repository is a fork of [react-umg](https://github.com/drywolf/react-umg) whose original author is [Wolfgang Steiner](https://github.com/drywolf) 4 | 5 | A React renderer for Unreal Motion Graphics (https://docs.unrealengine.com/latest/INT/Engine/UMG/) 6 | 7 | This project is dependent on [Unreal.js](https://github.com/ncsoft/Unreal.js) 8 | 9 | - [Link to Demo](https://github.com/ncsoft/Unreal.js-demo) 10 | 11 | ##### We recommend using React with [Babel](https://babeljs.io) to let you use JSX in your Javascript code. JSX is an extension to the Javascript language that works nicely with React. 12 | 13 | ### Install 14 | To install React-UMG with npm, run: 15 | 16 | `npm i --save react-umg` 17 | 18 | ### Web-dev like Component Naming 19 | 20 | - div(UVerticalBox) 21 | - span(UHorizontalBox) 22 | - text(UTextBlock) 23 | - img(UImage) 24 | - input(EditableText) 25 | 26 | ### Example 27 | 28 | #### Create Component 29 | 30 | ```js 31 | class MyComponent extends React.Component { 32 | constructor(props, context) { 33 | super(props, context); 34 | this.state = {text:"MyComponent"}; 35 | } 36 | 37 | OnTextChanged(value) { 38 | this.setState({text: value}); 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 | this.OnTextChanged(value)}/> 45 | 46 |
47 | ) 48 | } 49 | } 50 | ``` 51 | 52 | #### Draw With React-UMG 53 | 54 | ```js 55 | let widget = ReactUMG.wrap(); 56 | widget.AddToViewport(); 57 | return () => { 58 | widget.RemoveFromViewport(); 59 | } 60 | ``` 61 | 62 | - [Details](https://github.com/ncsoft/Unreal.js-demo/blob/master/Content/Scripts/demos/src/demo-react.jsx) 63 | 64 | 65 | ### License 66 | - Licensed under the BSD 3-Clause "New" or "Revised" License 67 | - see [LICENSE](https://github.com/ncsoft/React-UMG/blob/master/LICENSE) for details 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./src') 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-umg", 3 | "version": "0.2.6", 4 | "description": "A React renderer for Unreal Motion Graphics With Unreal.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ncsoft/React-UMG.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/ncsoft/React-UMG/issues" 15 | }, 16 | "keywords": [ 17 | "unreal.js", 18 | "react-umg", 19 | "react", 20 | "umg" 21 | ], 22 | "author": { 23 | "name": "NCSOFT", 24 | "email": "crocuis@ncsoft.com", 25 | "url": "http://ncsoft.com" 26 | }, 27 | "license": "MIT", 28 | "dependencies": { 29 | "fbjs": "^0.8.3", 30 | "lodash": "^4.17.4", 31 | "react": "<=15.4.2", 32 | "react-dom": "<=15.4.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ReactInstanceHandles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule ReactInstanceHandles 10 | */ 11 | 12 | 'use strict'; 13 | 14 | var _prodInvariant = require('./reactProdInvariant'); 15 | 16 | var invariant = require('fbjs/lib/invariant'); 17 | 18 | var SEPARATOR = '.'; 19 | var SEPARATOR_LENGTH = SEPARATOR.length; 20 | 21 | /** 22 | * Maximum depth of traversals before we consider the possibility of a bad ID. 23 | */ 24 | var MAX_TREE_DEPTH = 10000; 25 | 26 | /** 27 | * Creates a DOM ID prefix to use when mounting React components. 28 | * 29 | * @param {number} index A unique integer 30 | * @return {string} React root ID. 31 | * @internal 32 | */ 33 | function getReactRootIDString(index) { 34 | return SEPARATOR + index.toString(36); 35 | } 36 | 37 | /** 38 | * Checks if a character in the supplied ID is a separator or the end. 39 | * 40 | * @param {string} id A React DOM ID. 41 | * @param {number} index Index of the character to check. 42 | * @return {boolean} True if the character is a separator or end of the ID. 43 | * @private 44 | */ 45 | function isBoundary(id, index) { 46 | return id.charAt(index) === SEPARATOR || index === id.length; 47 | } 48 | 49 | /** 50 | * Checks if the supplied string is a valid React DOM ID. 51 | * 52 | * @param {string} id A React DOM ID, maybe. 53 | * @return {boolean} True if the string is a valid React DOM ID. 54 | * @private 55 | */ 56 | function isValidID(id) { 57 | return id === '' || id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR; 58 | } 59 | 60 | /** 61 | * Checks if the first ID is an ancestor of or equal to the second ID. 62 | * 63 | * @param {string} ancestorID 64 | * @param {string} descendantID 65 | * @return {boolean} True if `ancestorID` is an ancestor of `descendantID`. 66 | * @internal 67 | */ 68 | function isAncestorIDOf(ancestorID, descendantID) { 69 | return descendantID.indexOf(ancestorID) === 0 && isBoundary(descendantID, ancestorID.length); 70 | } 71 | 72 | /** 73 | * Gets the parent ID of the supplied React DOM ID, `id`. 74 | * 75 | * @param {string} id ID of a component. 76 | * @return {string} ID of the parent, or an empty string. 77 | * @private 78 | */ 79 | function getParentID(id) { 80 | return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : ''; 81 | } 82 | 83 | /** 84 | * Gets the next DOM ID on the tree path from the supplied `ancestorID` to the 85 | * supplied `destinationID`. If they are equal, the ID is returned. 86 | * 87 | * @param {string} ancestorID ID of an ancestor node of `destinationID`. 88 | * @param {string} destinationID ID of the destination node. 89 | * @return {string} Next ID on the path from `ancestorID` to `destinationID`. 90 | * @private 91 | */ 92 | function getNextDescendantID(ancestorID, destinationID) { 93 | !(isValidID(ancestorID) && isValidID(destinationID)) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', ancestorID, destinationID) : _prodInvariant('112', ancestorID, destinationID) : void 0; 94 | !isAncestorIDOf(ancestorID, destinationID) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'getNextDescendantID(...): React has made an invalid assumption about the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.', ancestorID, destinationID) : _prodInvariant('113', ancestorID, destinationID) : void 0; 95 | if (ancestorID === destinationID) { 96 | return ancestorID; 97 | } 98 | // Skip over the ancestor and the immediate separator. Traverse until we hit 99 | // another separator or we reach the end of `destinationID`. 100 | var start = ancestorID.length + SEPARATOR_LENGTH; 101 | var i; 102 | for (i = start; i < destinationID.length; i++) { 103 | if (isBoundary(destinationID, i)) { 104 | break; 105 | } 106 | } 107 | return destinationID.substr(0, i); 108 | } 109 | 110 | /** 111 | * Gets the nearest common ancestor ID of two IDs. 112 | * 113 | * Using this ID scheme, the nearest common ancestor ID is the longest common 114 | * prefix of the two IDs that immediately preceded a "marker" in both strings. 115 | * 116 | * @param {string} oneID 117 | * @param {string} twoID 118 | * @return {string} Nearest common ancestor ID, or the empty string if none. 119 | * @private 120 | */ 121 | function getFirstCommonAncestorID(oneID, twoID) { 122 | var minLength = Math.min(oneID.length, twoID.length); 123 | if (minLength === 0) { 124 | return ''; 125 | } 126 | var lastCommonMarkerIndex = 0; 127 | // Use `<=` to traverse until the "EOL" of the shorter string. 128 | for (var i = 0; i <= minLength; i++) { 129 | if (isBoundary(oneID, i) && isBoundary(twoID, i)) { 130 | lastCommonMarkerIndex = i; 131 | } else if (oneID.charAt(i) !== twoID.charAt(i)) { 132 | break; 133 | } 134 | } 135 | var longestCommonID = oneID.substr(0, lastCommonMarkerIndex); 136 | !isValidID(longestCommonID) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', oneID, twoID, longestCommonID) : _prodInvariant('114', oneID, twoID, longestCommonID) : void 0; 137 | return longestCommonID; 138 | } 139 | 140 | /** 141 | * Traverses the parent path between two IDs (either up or down). The IDs must 142 | * not be the same, and there must exist a parent path between them. If the 143 | * callback returns `false`, traversal is stopped. 144 | * 145 | * @param {?string} start ID at which to start traversal. 146 | * @param {?string} stop ID at which to end traversal. 147 | * @param {function} cb Callback to invoke each ID with. 148 | * @param {*} arg Argument to invoke the callback with. 149 | * @param {?boolean} skipFirst Whether or not to skip the first node. 150 | * @param {?boolean} skipLast Whether or not to skip the last node. 151 | * @private 152 | */ 153 | function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) { 154 | start = start || ''; 155 | stop = stop || ''; 156 | !(start !== stop) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start) : _prodInvariant('115', start) : void 0; 157 | var traverseUp = isAncestorIDOf(stop, start); 158 | !(traverseUp || isAncestorIDOf(start, stop)) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do not have a parent path.', start, stop) : _prodInvariant('116', start, stop) : void 0; 159 | // Traverse from `start` to `stop` one depth at a time. 160 | var depth = 0; 161 | var traverse = traverseUp ? getParentID : getNextDescendantID; 162 | for (var id = start;; /* until break */id = traverse(id, stop)) { 163 | var ret; 164 | if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { 165 | ret = cb(id, traverseUp, arg); 166 | } 167 | if (ret === false || id === stop) { 168 | // Only break //after// visiting `stop`. 169 | break; 170 | } 171 | !(depth++ < MAX_TREE_DEPTH) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'traverseParentPath(%s, %s, ...): Detected an infinite loop while traversing the React DOM ID tree. This may be due to malformed IDs: %s', start, stop, id) : _prodInvariant('117', start, stop, id) : void 0; 172 | } 173 | } 174 | 175 | /** 176 | * Manages the IDs assigned to DOM representations of React components. This 177 | * uses a specific scheme in order to traverse the DOM efficiently (e.g. in 178 | * order to simulate events). 179 | * 180 | * @internal 181 | */ 182 | var ReactInstanceHandles = { 183 | 184 | /** 185 | * Constructs a React root ID 186 | * @param {number} index A unique integer 187 | * @return {string} A React root ID. 188 | */ 189 | createReactRootID: function (index) { 190 | return getReactRootIDString(index); 191 | }, 192 | 193 | /** 194 | * Constructs a React ID by joining a root ID with a name. 195 | * 196 | * @param {string} rootID Root ID of a parent component. 197 | * @param {string} name A component's name (as flattened children). 198 | * @return {string} A React ID. 199 | * @internal 200 | */ 201 | createReactID: function (rootID, name) { 202 | return rootID + name; 203 | }, 204 | 205 | /** 206 | * Gets the DOM ID of the React component that is the root of the tree that 207 | * contains the React component with the supplied DOM ID. 208 | * 209 | * @param {string} id DOM ID of a React component. 210 | * @return {?string} DOM ID of the React component that is the root. 211 | * @internal 212 | */ 213 | getReactRootIDFromNodeID: function (id) { 214 | if (id && id.charAt(0) === SEPARATOR && id.length > 1) { 215 | var index = id.indexOf(SEPARATOR, 1); 216 | return index > -1 ? id.substr(0, index) : id; 217 | } 218 | return null; 219 | }, 220 | 221 | /** 222 | * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that 223 | * should would receive a `mouseEnter` or `mouseLeave` event. 224 | * 225 | * NOTE: Does not invoke the callback on the nearest common ancestor because 226 | * nothing "entered" or "left" that element. 227 | * 228 | * @param {string} leaveID ID being left. 229 | * @param {string} enterID ID being entered. 230 | * @param {function} cb Callback to invoke on each entered/left ID. 231 | * @param {*} upArg Argument to invoke the callback with on left IDs. 232 | * @param {*} downArg Argument to invoke the callback with on entered IDs. 233 | * @internal 234 | */ 235 | traverseEnterLeave: function (leaveID, enterID, cb, upArg, downArg) { 236 | var ancestorID = getFirstCommonAncestorID(leaveID, enterID); 237 | if (ancestorID !== leaveID) { 238 | traverseParentPath(leaveID, ancestorID, cb, upArg, false, true); 239 | } 240 | if (ancestorID !== enterID) { 241 | traverseParentPath(ancestorID, enterID, cb, downArg, true, false); 242 | } 243 | }, 244 | 245 | /** 246 | * Simulates the traversal of a two-phase, capture/bubble event dispatch. 247 | * 248 | * NOTE: This traversal happens on IDs without touching the DOM. 249 | * 250 | * @param {string} targetID ID of the target node. 251 | * @param {function} cb Callback to invoke. 252 | * @param {*} arg Argument to invoke the callback with. 253 | * @internal 254 | */ 255 | traverseTwoPhase: function (targetID, cb, arg) { 256 | if (targetID) { 257 | traverseParentPath('', targetID, cb, arg, true, false); 258 | traverseParentPath(targetID, '', cb, arg, false, true); 259 | } 260 | }, 261 | 262 | /** 263 | * Same as `traverseTwoPhase` but skips the `targetID`. 264 | */ 265 | traverseTwoPhaseSkipTarget: function (targetID, cb, arg) { 266 | if (targetID) { 267 | traverseParentPath('', targetID, cb, arg, true, true); 268 | traverseParentPath(targetID, '', cb, arg, true, true); 269 | } 270 | }, 271 | 272 | /** 273 | * Traverse a node ID, calling the supplied `cb` for each ancestor ID. For 274 | * example, passing `.0.$row-0.1` would result in `cb` getting called 275 | * with `.0`, `.0.$row-0`, and `.0.$row-0.1`. 276 | * 277 | * NOTE: This traversal happens on IDs without touching the DOM. 278 | * 279 | * @param {string} targetID ID of the target node. 280 | * @param {function} cb Callback to invoke. 281 | * @param {*} arg Argument to invoke the callback with. 282 | * @internal 283 | */ 284 | traverseAncestors: function (targetID, cb, arg) { 285 | traverseParentPath('', targetID, cb, arg, true, false); 286 | }, 287 | 288 | getFirstCommonAncestorID: getFirstCommonAncestorID, 289 | 290 | /** 291 | * Exposed for unit testing. 292 | * @private 293 | */ 294 | _getNextDescendantID: getNextDescendantID, 295 | 296 | isAncestorIDOf: isAncestorIDOf, 297 | 298 | SEPARATOR: SEPARATOR 299 | 300 | }; 301 | 302 | module.exports = ReactInstanceHandles; -------------------------------------------------------------------------------- /src/ReactUMGComponent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ReactMultiChild = require('react-dom/lib/ReactMultiChild'); 4 | const ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); 5 | const invariant = require('fbjs/lib/invariant'); 6 | const warning = require('fbjs/lib/warning'); 7 | const shallowEqual = require('fbjs/lib/shallowEqual'); 8 | const UmgRoots = require('./UMGRoots'); 9 | const TypeThunks = require('./components/ReactUMGClassMap'); 10 | 11 | // In some cases we might not have a owner and when 12 | // that happens there is no need to inlcude "Check the render method of ...". 13 | const checkRenderMethod = () => ReactCurrentOwner.owner && ReactCurrentOwner.owner.getName() 14 | ? ` Check the render method of "${ReactCurrentOwner.owner.getName()}".` : ''; 15 | 16 | /** 17 | * @constructor ReactUMGComponent 18 | * @extends ReactComponent 19 | * @extends ReactMultiChild 20 | */ 21 | const ReactUMGComponent = function(element) { 22 | this.node = null; 23 | this._mountImage = null; 24 | this._renderedChildren = null; 25 | this._currentElement = element; 26 | this.ueobj = null; 27 | 28 | this._rootNodeID = null; 29 | this._typeThunk = TypeThunks[element.type]; 30 | 31 | if (process.env.NODE_ENV !== 'production') { 32 | warning( 33 | Object.keys(TypeThunks).indexOf(element.type) > -1, 34 | 'Attempted to render an unsupported generic component "%s". ' + 35 | 'Must be one of the following: ' + Object.keys(TypeThunks), 36 | element.type, 37 | checkRenderMethod() 38 | ); 39 | } 40 | }; 41 | 42 | /** 43 | * Mixin for UMG components. 44 | */ 45 | ReactUMGComponent.Mixin = { 46 | getHostNode() {}, 47 | 48 | getPublicInstance() { 49 | // TODO: This should probably use a composite wrapper 50 | return this; 51 | }, 52 | 53 | unmountComponent() { 54 | if (this.ueobj) { 55 | if (this._currentElement.props.$unlink) { 56 | this._currentElement.props.$unlink(this.ueobj); 57 | } 58 | for (var key in this._currentElement.props) { 59 | if (typeof this._currentElement.props[key] === 'function') { 60 | this.updateProperty(this.ueobj, null, key); 61 | } 62 | } 63 | this.ueobj.RemoveFromParent(); 64 | } 65 | 66 | this.unmountChildren(); 67 | this._rootNodeID = null; 68 | this.ueobj = null; 69 | }, 70 | updateProperty(widget, value, key) { 71 | this._typeThunk.applyProperty(widget,value, key); 72 | }, 73 | sync() { 74 | JavascriptWidget.CallSynchronizeProperties(this.ueobj) 75 | JavascriptWidget.CallSynchronizeProperties(this.ueobj.Slot) 76 | }, 77 | mountComponent( 78 | transaction, // for creating/updating 79 | rootID, // Root ID of this subtree 80 | hostContainerInfo, // nativeContainerInfo 81 | context // secret context, shhhh 82 | ) { 83 | var parent = rootID; 84 | 85 | rootID = typeof rootID === 'object' ? rootID._rootNodeID : rootID; 86 | this._rootNodeID = rootID; 87 | 88 | var umgRoot = parent.ueobj ? parent.ueobj : UmgRoots[rootID]; 89 | if (umgRoot instanceof JavascriptWidget) { 90 | umgRoot = umgRoot.WidgetTree.RootWidget 91 | } 92 | var outer = Root.GetEngine ? JavascriptLibrary.CreatePackage(null,'/Script/Javascript') : GWorld 93 | 94 | this.ueobj = this._typeThunk.createUmgElement( 95 | this._currentElement, 96 | cls => { 97 | var widget = new cls(outer); 98 | var props = this._currentElement.props; 99 | for (var key in props) { 100 | this.updateProperty(widget, props[key], key); 101 | } 102 | if (widget instanceof JavascriptWidget) { 103 | widget.AddChild(new SizeBox(outer)); 104 | } 105 | if (umgRoot['AddChild'] != null) { 106 | var slot = umgRoot.AddChild(widget); 107 | if (slot) 108 | slot.Content.Slot = slot; 109 | return widget; 110 | } 111 | else { 112 | console.error('cannot add child', umgRoot); 113 | } 114 | } 115 | ); 116 | 117 | this.sync(); 118 | 119 | if (this._currentElement.props.$link) { 120 | this._currentElement.props.$link(this.ueobj); 121 | } 122 | this.initializeChildren( 123 | this._currentElement.props.children, 124 | transaction, 125 | context 126 | ); 127 | return rootID; 128 | }, 129 | 130 | /** 131 | * Updates the component's currently mounted representation. 132 | */ 133 | receiveComponent( 134 | nextElement, transaction, context) { 135 | const prevElement = this._currentElement; 136 | this._currentElement = nextElement; 137 | this.updateComponent(transaction, prevElement, nextElement, context); 138 | }, 139 | updateComponent( 140 | transaction, prevElement, nextElement, context) { 141 | var lastProps = prevElement.props; 142 | var nextProps = nextElement.props; 143 | if (!shallowEqual(lastProps, nextProps)) { 144 | this.updateProperties(lastProps, nextProps, transaction) 145 | } 146 | this.updateChildren(nextProps.children, transaction, context); 147 | }, 148 | updateProperties( 149 | lastProps, nextProps, transaction) { 150 | for (var propKey in nextProps) { 151 | var nextProp = nextProps[propKey]; 152 | var lastProp = lastProps != null ? lastProps[propKey] : undefined; 153 | if (!nextProps.hasOwnProperty(propKey) || 154 | nextProp === lastProp || 155 | nextProp == null && lastProp == null) { 156 | continue; 157 | } 158 | this.updateProperty(this.ueobj, nextProp, propKey); 159 | } 160 | }, 161 | initializeChildren( 162 | children, transaction, context) { 163 | this.mountChildren(children, transaction, context); 164 | }, 165 | }; 166 | 167 | /** 168 | * Order of mixins is important. ReactUMGComponent overrides methods in 169 | * ReactMultiChild. 170 | */ 171 | Object.assign( 172 | ReactUMGComponent.prototype, 173 | ReactMultiChild.Mixin, 174 | ReactUMGComponent.Mixin 175 | ); 176 | 177 | module.exports = ReactUMGComponent; 178 | -------------------------------------------------------------------------------- /src/ReactUMGDefaultInjection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * React UMG Default Injection 5 | */ 6 | require('./devtools/InitializeJavaScriptAppEngine'); 7 | const ReactInjection = require('react-dom/lib/ReactInjection'); 8 | const ReactDefaultBatchingStrategy = require('react-dom/lib/ReactDefaultBatchingStrategy'); 9 | const ReactComponentEnvironment = require('react-dom/lib/ReactComponentEnvironment'); 10 | const ReactUMGReconcileTransaction = require('./ReactUMGReconcileTransaction'); 11 | const ReactUMGComponent = require('./ReactUMGComponent'); 12 | const ReactUMGEmptyComponent = require('./ReactUMGEmptyComponent'); 13 | var alreadyInjected = false; 14 | 15 | function inject() { 16 | if (alreadyInjected) { 17 | // TODO: This is currently true because these injections are shared between 18 | // the client and the server package. They should be built independently 19 | // and not share any injection state. Then this problem will be solved. 20 | return; 21 | } 22 | alreadyInjected = true; 23 | 24 | ReactInjection.HostComponent.injectGenericComponentClass( 25 | ReactUMGComponent 26 | ); 27 | 28 | // // Maybe? 29 | ReactInjection.HostComponent.injectTextComponentClass( 30 | function(instantiate) {return new ReactUMGEmptyComponent(instantiate)} 31 | ); 32 | 33 | ReactInjection.Updates.injectReconcileTransaction( 34 | ReactUMGReconcileTransaction 35 | ); 36 | 37 | ReactInjection.Updates.injectBatchingStrategy( 38 | ReactDefaultBatchingStrategy 39 | ); 40 | 41 | ReactInjection.EmptyComponent.injectEmptyComponentFactory( 42 | function(instantiate){ return new ReactUMGEmptyComponent(instantiate) } 43 | ); 44 | 45 | ReactComponentEnvironment.processChildrenUpdates = function() {}; 46 | ReactComponentEnvironment.replaceNodeWithMarkup = function() {}; 47 | ReactComponentEnvironment.unmountIDFromEnvironment = function() {}; 48 | } 49 | 50 | module.exports = inject -------------------------------------------------------------------------------- /src/ReactUMGEmptyComponent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ReactMultiChild = require('react-dom/lib/ReactMultiChild'); 4 | 5 | const ReactUMGEmptyComponent = function(element) { 6 | 7 | this.node = null; 8 | this._mountImage = null; 9 | this._renderedChildren = null; 10 | this._currentElement = element; 11 | this._rootNodeID = null; 12 | }; 13 | 14 | ReactUMGEmptyComponent.prototype = Object.assign( 15 | { 16 | construct(element) {}, 17 | 18 | getPublicInstance() {}, 19 | mountComponent() {}, 20 | receiveComponent() {}, 21 | unmountComponent() {}, 22 | // Implement both of these for now. React <= 15.0 uses getNativeNode, but 23 | // that is confusing. Host environment is more accurate and will be used 24 | // going forward 25 | getNativeNode() {}, 26 | getHostNode() {} 27 | }, 28 | ReactMultiChild.Mixin 29 | ); 30 | 31 | module.exports = ReactUMGEmptyComponent; 32 | -------------------------------------------------------------------------------- /src/ReactUMGMount.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ReactElement = require('react/lib/ReactElement'); 4 | const ReactInstanceMap = require('react-dom/lib/ReactInstanceMap'); 5 | const ReactUpdates = require('react-dom/lib/ReactUpdates'); 6 | const ReactUpdateQueue = require('react-dom/lib/ReactUpdateQueue'); 7 | const ReactReconciler = require('react-dom/lib/ReactReconciler'); 8 | const shouldUpdateReactComponent = require('react-dom/lib/shouldUpdateReactComponent'); 9 | const instantiateReactComponent = require('react-dom/lib/instantiateReactComponent'); 10 | 11 | const invariant = require('fbjs/lib/invariant'); 12 | const warning = require('fbjs/lib/warning'); 13 | 14 | const ReactInstanceHandles = require('./ReactInstanceHandles'); 15 | const ReactUMGDefaultInjection = require('./ReactUMGDefaultInjection'); 16 | 17 | ReactUMGDefaultInjection(); 18 | // TODO: externalize management of UMG node meta-data (id, component, ...) 19 | let idCounter = 1; 20 | 21 | const UmgRoots = require('./UMGRoots'); 22 | const TypeThunks = require('./components/ReactUMGClassMap'); 23 | const NodeMap = require('./ReactUMGNodeMap'); 24 | 25 | function isString(x) { 26 | return Object.prototype.toString.call(x) === "[object String]" 27 | } 28 | 29 | const ReactUMGMount = { 30 | // for react devtools 31 | _instancesByReactRootID: {}, 32 | nativeTagToRootNodeID(nativeTag) { 33 | throw new Error('TODO: implement nativeTagToRootNodeID ' + nativeTag); 34 | }, 35 | 36 | /** 37 | * Renders a React component to the supplied `container` port. 38 | * 39 | * If the React component was previously rendered into `container`, this will 40 | * perform an update on it and only mutate the pins as necessary to reflect 41 | * the latest React component. 42 | */ 43 | render( 44 | nextElement, 45 | umgWidget, 46 | callback 47 | ) { 48 | // WIP: it appears as though nextElement.props is an empty object... 49 | invariant( 50 | ReactElement.isValidElement(nextElement), 51 | 'ReactUMG.render(): Invalid component element.%s', 52 | ( 53 | typeof nextElement === 'function' ? 54 | ' Instead of passing a component class, make sure to instantiate ' + 55 | 'it by passing it to React.createElement.' : 56 | // Check if it quacks like an element 57 | nextElement != null && nextElement.props !== undefined ? 58 | ' This may be caused by unintentionally loading two independent ' + 59 | 'copies of React.' : 60 | '' 61 | ) 62 | ); 63 | if (umgWidget) { 64 | const prevComponent = umgWidget.component; 65 | if (prevComponent) { 66 | const prevWrappedElement = prevComponent._currentElement; 67 | const prevElement = prevWrappedElement.props; 68 | if (shouldUpdateReactComponent(prevElement, nextElement)) { 69 | const publicInst = prevComponent._renderedComponent.getPublicInstance(); 70 | const updatedCallback = callback && function() { 71 | if (callback) { 72 | callback.call(publicInst); 73 | } 74 | }; 75 | 76 | ReactUMGMount._updateRootComponent( 77 | prevComponent, 78 | nextElement, 79 | container, 80 | updatedCallback 81 | ); 82 | return publicInst; 83 | } else { 84 | warning( 85 | true, 86 | 'Unexpected `else` branch in ReactUMG.render()' 87 | ); 88 | } 89 | } 90 | } 91 | if (!umgWidget.reactUmgId) 92 | umgWidget.reactUmgId = idCounter++; 93 | 94 | const rootId = ReactInstanceHandles.createReactRootID(umgWidget.reactUmgId); 95 | 96 | let umgRoot = UmgRoots[rootId]; 97 | if (!umgRoot) { 98 | let type = nextElement.type; 99 | // pure react component 100 | if (isString(type) == false) { 101 | type = 'uSizeBox' 102 | } 103 | let typeThunk = TypeThunks[type]; 104 | let outer = Root.GetEngine ? JavascriptLibrary.CreatePackage(null,'/Script/Javascript') : GWorld; 105 | umgRoot = typeThunk.createUmgElement(nextElement, cls => new cls(outer)); 106 | umgWidget.AddChild(umgRoot); 107 | 108 | UmgRoots[rootId] = umgRoot; 109 | } 110 | const nextComponent = instantiateReactComponent(nextElement); 111 | 112 | if (!umgWidget.component) { 113 | umgWidget.component = nextComponent; 114 | } 115 | 116 | ReactUpdates.batchedUpdates(() => { 117 | // Two points to React for using object pooling internally and being good 118 | // stewards of garbage collection and memory pressure. 119 | const transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); 120 | transaction.perform(() => { 121 | // The `component` here is an instance of your 122 | // `ReactCustomRendererComponent` class. To be 100% honest, I’m not 123 | // certain if the method signature is enforced by React core or if it is 124 | // renderer specific. This is following the ReactDOM renderer. The 125 | // important piece is that we pass our transaction and rootId through, in 126 | // addition to any other contextual information needed. 127 | 128 | nextComponent.mountComponent( 129 | transaction, 130 | rootId, 131 | // TODO: what is _idCounter used for and when should it be nonzero? 132 | {_idCounter: 0}, 133 | {} 134 | ); 135 | if (callback) { 136 | callback(nextComponent.getPublicInstance()); 137 | } 138 | }); 139 | ReactUpdates.ReactReconcileTransaction.release(transaction); 140 | }); 141 | 142 | // needed for react-devtools 143 | ReactUMGMount._instancesByReactRootID[rootId] = nextComponent; 144 | NodeMap.set(nextComponent, umgWidget); 145 | 146 | umgWidget.JavascriptContext = Context; 147 | umgWidget.proxy = { 148 | OnDestroy: (bReleaseChildren) => { 149 | if (nextComponent.getPublicInstance()) { 150 | ReactUMGMount.unmountComponent(nextComponent.getPublicInstance()) 151 | } 152 | } 153 | } 154 | 155 | return nextComponent.getPublicInstance(); 156 | }, 157 | 158 | /** 159 | * Take a component that’s already mounted and replace its props 160 | */ 161 | _updateRootComponent( 162 | prevComponent, // component instance already in the DOM 163 | nextElement, // component instance to render 164 | container, // firmata connection port 165 | callback // function triggered on completion 166 | ) { 167 | ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement); 168 | if (callback) { 169 | ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback); 170 | } 171 | 172 | return prevComponent; 173 | }, 174 | 175 | renderComponent( 176 | rootID, 177 | container, 178 | nextComponent, 179 | nextElement, 180 | board, // Firmata instnace 181 | callback 182 | ) { 183 | 184 | const component = nextComponent || instantiateReactComponent(nextElement); 185 | 186 | // The initial render is synchronous but any updates that happen during 187 | // rendering, in componentWillMount or componentDidMount, will be batched 188 | // according to the current batching strategy. 189 | ReactUpdates.batchedUpdates(() => { 190 | // Batched mount component 191 | const transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); 192 | transaction.perform(() => { 193 | 194 | component.mountComponent( 195 | transaction, 196 | rootID, 197 | {_idCounter: 0}, 198 | {} 199 | ); 200 | if (callback) { 201 | const publicInst = component.getPublicInstance(); 202 | callback(publicInst); 203 | } 204 | }); 205 | ReactUpdates.ReactReconcileTransaction.release(transaction); 206 | }); 207 | 208 | return component.getPublicInstance(); 209 | }, 210 | unmountComponent(publicInstance) { 211 | const internalInstance = ReactUMGMount.getInternalInstance(publicInstance); 212 | let widget = NodeMap.get(internalInstance); 213 | if (widget) { 214 | const rootId = ReactInstanceHandles.createReactRootID(widget.reactUmgId); 215 | delete UmgRoots[rootId]; 216 | delete ReactUMGMount._instancesByReactRootID[rootId]; 217 | NodeMap.delete(internalInstance); 218 | } 219 | 220 | internalInstance.unmountComponent(); 221 | }, 222 | getInternalInstance(publicInstance) { 223 | // Reverse of ReactCompositeComponent(Wrapper).getPublicInstance 224 | let internalInstance = ReactInstanceMap.get(publicInstance); 225 | if (!internalInstance) { 226 | // Reverse of ReactUMGComponent.getPublicInstance 227 | internalInstance = publicInstance; 228 | } 229 | return internalInstance; 230 | }, 231 | findNode(publicInstance) { 232 | const internalInstance = ReactUMGMount.getInternalInstance(publicInstance) 233 | return internalInstance && NodeMap.get(internalInstance); 234 | }, 235 | wrap(nextElement, outer = Root.GetEngine ? JavascriptLibrary.CreatePackage(null,'/Script/Javascript') : GWorld) { 236 | let widget = Root.GetEngine ? new JavascriptWidget(outer) : GWorld.CreateWidget(JavascriptWidget); 237 | let publicInstance = ReactUMGMount.render(nextElement, widget); 238 | return ReactUMGMount.findNode(publicInstance); 239 | } 240 | }; 241 | 242 | module.exports = ReactUMGMount; 243 | -------------------------------------------------------------------------------- /src/ReactUMGNodeMap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = new Map(); 4 | -------------------------------------------------------------------------------- /src/ReactUMGReconcileTransaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CallbackQueue = require('react-dom/lib/CallbackQueue'); 4 | var PooledClass = require('react/lib/PooledClass'); 5 | var Transaction = require('react-dom/lib/Transaction'); 6 | var ReactUpdateQueue = require('react-dom/lib/ReactUpdateQueue'); 7 | 8 | /** 9 | * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during 10 | * the performing of the transaction. 11 | */ 12 | var ON_UMG_READY_QUEUEING = { 13 | /** 14 | * Initializes the internal firmata `connected` queue. 15 | */ 16 | initialize: function() { 17 | this.reactMountReady.reset(); 18 | }, 19 | 20 | /** 21 | * After Hardware is connected, invoke all registered `ready` callbacks. 22 | */ 23 | close: function() { 24 | this.reactMountReady.notifyAll(); 25 | }, 26 | }; 27 | 28 | /** 29 | * Executed within the scope of the `Transaction` instance. Consider these as 30 | * being member methods, but with an implied ordering while being isolated from 31 | * each other. 32 | */ 33 | var TRANSACTION_WRAPPERS = [ON_UMG_READY_QUEUEING]; 34 | 35 | function ReactUMGReconcileTransaction() { 36 | this.reinitializeTransaction(); 37 | this.reactMountReady = CallbackQueue.getPooled(null); 38 | } 39 | 40 | const Mixin = { 41 | /** 42 | * @see Transaction 43 | * @abstract 44 | * @final 45 | * @return {array} List of operation wrap procedures. 46 | */ 47 | getTransactionWrappers: function() { 48 | return TRANSACTION_WRAPPERS; 49 | }, 50 | 51 | /** 52 | * @return {object} The queue to collect `ready` callbacks with. 53 | */ 54 | getReactMountReady: function() { 55 | return this.reactMountReady; 56 | }, 57 | 58 | getUpdateQueue: function() { 59 | return ReactUpdateQueue; 60 | }, 61 | 62 | /** 63 | * `PooledClass` looks for this, and will invoke this before allowing this 64 | * instance to be resused. 65 | */ 66 | destructor: function() { 67 | CallbackQueue.release(this.reactMountReady); 68 | this.reactMountReady = null; 69 | }, 70 | }; 71 | 72 | Object.assign( 73 | ReactUMGReconcileTransaction.prototype, 74 | Transaction, 75 | ReactUMGReconcileTransaction, 76 | Mixin 77 | ); 78 | 79 | PooledClass.addPoolingTo(ReactUMGReconcileTransaction); 80 | 81 | module.exports = ReactUMGReconcileTransaction; 82 | 83 | -------------------------------------------------------------------------------- /src/UMGRoots.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /src/components/ReactUMGClassMap.js: -------------------------------------------------------------------------------- 1 | module.exports = {} -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require('lodash'); 4 | const ReactUMGClassMap = require('./ReactUMGClassMap'); 5 | const ClassMap = JavascriptLibrary.GetDerivedClasses(Widget, [], true) 6 | const {set_attrs, set_attr} = require('./set_attrs'); 7 | 8 | const mappingTable = { 9 | 'VerticalBox': 'div', 10 | 'HorizontalBox': 'span', 11 | 'TextBlock': 'text', 12 | 'Image': 'img', 13 | 'EditableText': 'input' 14 | } 15 | 16 | function registerComponent(key, cls) { 17 | class klass { 18 | static createUmgElement(element, instantiator) { 19 | let elem = instantiator(cls); 20 | let props = _.pickBy(element.props, (v, k) => k != 'children') 21 | set_attrs(elem, props) 22 | return elem 23 | } 24 | 25 | static applyProperty(umgElem, value, key) { 26 | if (!umgElem) return; 27 | if (key != 'children') { 28 | set_attr(umgElem, key, value) 29 | } 30 | } 31 | } 32 | ReactUMGClassMap[key] = klass; 33 | } 34 | 35 | ClassMap.Results.forEach(cls => { 36 | const key = _.first(_.last(JavascriptLibrary.GetClassPathName(cls).split('.')).split('_')); 37 | if (mappingTable[key]) { 38 | registerComponent(mappingTable[key], cls) 39 | } 40 | registerComponent('u' + key, cls) 41 | }) 42 | 43 | module.exports = { 44 | Register: registerComponent 45 | }; -------------------------------------------------------------------------------- /src/components/set_attrs.js: -------------------------------------------------------------------------------- 1 | function set_attrs(instance, attrs) { 2 | for (var k in attrs) { 3 | set_attr(instance, k, attrs[k]); 4 | } 5 | } 6 | 7 | function set_attr(instance, k, attr) { 8 | function inner(k) { 9 | var setter = instance["Set" + k] 10 | if (setter != undefined) { 11 | setter.call(instance, attr) 12 | return true 13 | } else if (instance[k] != undefined) { 14 | if (instance[k] instanceof UObject) { 15 | set_attrs(instance[k], attr) 16 | return true 17 | } else { 18 | try { 19 | instance[k] = attr 20 | } catch (e) { 21 | console.error(String(e), k, JSON.stringify(attr)) 22 | } 23 | 24 | return true 25 | } 26 | } else { 27 | return false 28 | } 29 | } 30 | 31 | inner(k) || inner("b" + k) 32 | } 33 | module.exports = { 34 | set_attrs: set_attrs, 35 | set_attr: set_attr 36 | } -------------------------------------------------------------------------------- /src/devtools/InitializeJavaScriptAppEngine.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const setupDevtools = require('./setupDevtools'); 4 | 5 | if (typeof GLOBAL === 'undefined') { 6 | global.GLOBAL = this; 7 | } 8 | 9 | if (typeof window === 'undefined') { 10 | global.window = GLOBAL; 11 | } 12 | 13 | if (!window || !window.document) { 14 | setupDevtools(); 15 | } 16 | 17 | if (typeof process === 'undefined') { 18 | global.process = { env: 'development' }; 19 | } 20 | -------------------------------------------------------------------------------- /src/devtools/setupDevtools.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function setupDevtools() { 4 | var messageListeners = []; 5 | var closeListeners = []; 6 | //var ws = new WS('ws://localhost:8097/devtools'); 7 | // this is accessed by the eval'd backend code 8 | var FOR_BACKEND = { // eslint-disable-line no-unused-vars 9 | wall: { 10 | listen(fn) { 11 | messageListeners.push(fn); 12 | }, 13 | onClose(fn) { 14 | closeListeners.push(fn); 15 | }, 16 | send(data) { 17 | console.log('sending\n%s\n', JSON.stringify(data, null, 2)); 18 | //ws.send(JSON.stringify(data)); 19 | }, 20 | }, 21 | }; 22 | } 23 | 24 | module.exports = setupDevtools; 25 | -------------------------------------------------------------------------------- /src/editor-maker.js: -------------------------------------------------------------------------------- 1 | const outer = JavascriptLibrary.CreatePackage(null, '/Script/Javascript'); 2 | 3 | function maybe_create_group(path) { 4 | let groups = global.$groups = global.$groups || {}; 5 | if (!groups[path]) { 6 | let cur = JavascriptEditorLibrary; 7 | path.split('.').forEach((v, k) => { 8 | console.log(cur, k); 9 | let x = cur.GetGroup && cur.GetGroup(v); 10 | if (!x) { 11 | x = cur.AddGroup(v); 12 | } 13 | cur = x; 14 | }); 15 | groups[path] = cur; 16 | } 17 | return groups[path]; 18 | } 19 | 20 | function makeTab(opts, tab_fn, del_fn) { 21 | opts = opts || {}; 22 | var tab = new JavascriptEditorTab(); 23 | tab.TabId = opts.TabId || 'TestJSTab'; 24 | tab.Role = opts.Role || 'NomadTab'; 25 | tab.DisplayName = opts.DisplayName || '안녕하세요!'; 26 | tab.Group = opts.Group || global.group; 27 | tab.OnSpawnTab.Add(tab_fn); 28 | if (del_fn) { 29 | tab.OnCloseTab.Add(del_fn); 30 | } 31 | return tab; 32 | } 33 | 34 | function tabSpawner(opts, main) { 35 | let $tabs = global.$tabs = global.$tabs || {}; 36 | let $inner = global.$tabinner = global.$tabinner || []; 37 | let $fns = global.$tabfns = global.$tabfns || {}; 38 | const id = opts.TabId; 39 | 40 | $fns[id] = main; 41 | let opened = $tabs[id]; 42 | 43 | function create_inner(fn, where) { 44 | let child; 45 | try { 46 | child = fn(); 47 | } catch (e) { 48 | console.error(String(e), e.stack); 49 | child = new TextBlock(); 50 | child.SetText(`ERROR:${String(e)}`); 51 | } 52 | $inner.push(child); 53 | where.AddChild(child); 54 | } 55 | if (opened) { 56 | opened.forEach(open => { 57 | let old = SizeBox.C(open).GetChildAt(0); 58 | old.RemoveFromParent(); 59 | $inner.splice($inner.indexOf(old), 1); 60 | SizeBox.C(open).RemoveChildAt(0); 61 | create_inner(main, open); 62 | }); 63 | return _ => {}; 64 | } 65 | 66 | opened = $tabs[id] = []; 67 | 68 | let tab = makeTab(opts, context => { 69 | let widget = new SizeBox(); 70 | let fn = $fns[id]; 71 | opened.push(widget); 72 | create_inner(fn, widget); 73 | 74 | return widget; 75 | }, widget => { 76 | let content = widget.GetContentSlot().Content; 77 | content.RemoveFromParent(); 78 | $inner.splice($inner.indexOf(widget.GetChildAt(0)), 1); 79 | opened.splice(opened.indexOf(widget), 1); 80 | }); 81 | tab.Commit(); 82 | 83 | opened.$spawner = tab; 84 | } 85 | 86 | 87 | function windowSpawner(opts, design) { 88 | let container = new JavascriptWindow(outer); 89 | container.SizingRule = EJavascriptSizingRule.Autosized; 90 | container.Title = opts.Title || 'Window' 91 | container.AddChild(design()); 92 | container.TakeWidget().AddWindow(); 93 | process.nextTick(_ => container.BringToFront()) 94 | return () => { 95 | container.RequestDestroyWindow(); 96 | } 97 | } 98 | 99 | module.exports = { 100 | spawnTab : (design, opts = {}) => { 101 | tabSpawner({ 102 | DisplayName: opts.Title, 103 | TabId: opts.TabId, 104 | Group: maybe_create_group(opts.Group || 'Root.A2') 105 | }, design); 106 | }, 107 | spawnWindow: (design, opts = {}) => { 108 | return windowSpawner({ 109 | Title: opts.Title 110 | }, design) 111 | } 112 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const ReactUMGMount = require('./ReactUMGMount'); 4 | const ReactUMGComponents = require('./components'); 5 | const EditorMaker = require('./editor-maker'); 6 | 7 | module.exports = { 8 | render: ReactUMGMount.render, 9 | wrap: ReactUMGMount.wrap, 10 | unmountComponent: ReactUMGMount.unmountComponent, 11 | Register: ReactUMGComponents.Register, 12 | spawnTab : EditorMaker.spawnTab, 13 | spawnWindow : EditorMaker.spawnWindow 14 | } --------------------------------------------------------------------------------