├── readme └── preview.png ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json ├── src ├── types.ts ├── convert-icon │ └── main.ts ├── main.ts └── ui.tsx └── .gitignore /readme/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdmvision/unity-figma-importer-plugin/HEAD/readme/preview.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@create-figma-plugin/tsconfig", 3 | "compilerOptions": { 4 | "typeRoots": ["node_modules/@figma", "node_modules/@types"] 5 | }, 6 | "include": ["src/**/*.ts", "src/**/*.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CDMVision 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Figma Importer Plugin 2 | 3 | Plugin URL: https://www.figma.com/community/plugin/1047282855279327962 4 | 5 | The Unity Figma importer plugin is designed to facilitate the process of saving Unity-specific data directly onto Figma nodes. This data can then be utilized during the import process into Unity, streamlining the workflow between design and development teams. This plugin runs on Figma app. 6 | 7 | **You also need to use [Unity Figma Importer](https://github.com/cdmvision/unity-figma-importer.git) package to import your Figma design into Unity.** 8 | 9 | ### Installation from Figma Community 10 | * Open Figma. 11 | * Navigate to the `Plugins` menu. 12 | * Click on `Manage Plugins`. 13 | * Search for `Unity Figma Importer` in the search bar. 14 | * Click on `Run` to add the plugin to your Figma workspace. 15 | 16 | ### How to build for development 17 | * Go to project directory and run: 18 | * `npm install` 19 | * `npm run build` 20 | * Open Figma desktop app and select `Plugins > New > Import plugin from manifest...` 21 | * Select the plugin `manifest.json` and done! 22 | 23 | ### License 24 | This plugin is licensed under the [MIT License](LICENSE). 25 | 26 | ![Preview](readme/preview.png) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@create-figma-plugin/ui": "3.1.0", 4 | "@create-figma-plugin/utilities": "3.1.0", 5 | "preact": "10.15.0" 6 | }, 7 | "devDependencies": { 8 | "@create-figma-plugin/build": "3.1.0", 9 | "@create-figma-plugin/tsconfig": "3.1.0", 10 | "@figma/plugin-typings": "1.88.0", 11 | "typescript": "4.9" 12 | }, 13 | "scripts": { 14 | "build": "build-figma-plugin --typecheck --minify", 15 | "watch": "build-figma-plugin --typecheck --watch" 16 | }, 17 | "figma-plugin": { 18 | "editorType": [ 19 | "figma" 20 | ], 21 | "id": "1047282855279327962", 22 | "name": "Unity Importer", 23 | "api": "1.88.0", 24 | "documentAccess": "dynamic-page", 25 | "networkAccess": { 26 | "allowedDomains": ["none"] 27 | }, 28 | "menu": [ 29 | { 30 | "name": "Unity Importer", 31 | "main": "src/main.ts", 32 | "ui": "src/ui.tsx", 33 | "parameterOnly": false 34 | }, 35 | "-", 36 | { 37 | "name": "Convert to Icon", 38 | "main": "src/convert-icon/main.ts", 39 | "parameterOnly": false, 40 | "parameters": [ 41 | { 42 | "key": "size", 43 | "description": "Enter an icon size", 44 | "allowFreeform": true, 45 | "optional": true 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export const events = { 2 | selectedNodeUpdated: 'selectedNodeUpdated', 3 | refreshUI: 'refreshUI', 4 | selectNode: 'selectNode', 5 | showWarnings : 'showWarnings', 6 | hideWarnings : 'hideWarnings' 7 | }; 8 | 9 | export const pluginData = { 10 | ignored: 'ignored', 11 | bindingKey: 'bindingKey', 12 | localizationKey: 'localizationKey', 13 | componentType: 'componentType', 14 | componentData: 'componentData', 15 | tags: 'tags' 16 | }; 17 | 18 | export class Warning { 19 | public message: string; 20 | public node: SceneNode; 21 | public nodeId: string; 22 | public nodeName: string; 23 | 24 | constructor(message: string, node: SceneNode) { 25 | this.message = message; 26 | this.node = node; 27 | this.nodeId = node.id; 28 | this.nodeName = node.name; 29 | } 30 | } 31 | 32 | export class NodeMetadata { 33 | public id: string; 34 | public type: string; 35 | public name: string; 36 | public ignored:boolean; 37 | public bindingKey: string; 38 | public localizationKey: string; 39 | public componentType: string; 40 | public componentData: object | null; 41 | public tags: string; 42 | 43 | public warnings: Warning[]; 44 | 45 | constructor() { 46 | this.id = ''; 47 | this.type = ''; 48 | this.name = ''; 49 | this.ignored = false; 50 | this.bindingKey = ''; 51 | this.localizationKey = ''; 52 | this.componentType = ''; 53 | this.componentData = null; 54 | this.tags = ''; 55 | this.warnings = []; 56 | } 57 | } 58 | 59 | export function serializeMetadata(metadata: NodeMetadata | null) : string { 60 | return JSON.stringify(metadata); 61 | } 62 | 63 | export function deserializeMetadata(json: string): NodeMetadata | null { 64 | if (json != null && json.length > 0) 65 | { 66 | return JSON.parse(json); 67 | } 68 | return null; 69 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | build/ 10 | .vscode/ 11 | manifest.json 12 | package-lock.json 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | -------------------------------------------------------------------------------- /src/convert-icon/main.ts: -------------------------------------------------------------------------------- 1 | export default function (s?:Number) { 2 | figma.parameters.on( 3 | 'input', 4 | ({ parameters, key, query, result }: ParameterInputEvent) => { 5 | switch (key) { 6 | case 'size': 7 | const icons = [ 8 | '24', 9 | '32', 10 | '48', 11 | '64', 12 | '96', 13 | '128', 14 | '256', 15 | '512', 16 | ] 17 | result.setSuggestions(icons.filter(s => s.includes(query))); 18 | break; 19 | 20 | default: 21 | return; 22 | } 23 | } 24 | ) 25 | 26 | figma.on('run', ({ command, parameters }: RunEvent) => { 27 | if (!parameters || !parameters.size) 28 | { 29 | parameters = { size: "24" }; 30 | } 31 | 32 | convertToIcon(parseFloat(parameters.size)); 33 | figma.closePlugin(); 34 | }); 35 | } 36 | 37 | function convertToIcon(iconSize:number) 38 | { 39 | const nodes = figma.currentPage.selection; 40 | 41 | if (nodes.length == 0) { 42 | figma.notify("There is no node selected."); 43 | return; 44 | } 45 | 46 | const position = { x:Number.MAX_VALUE, y:Number.MAX_VALUE}; 47 | 48 | for (const node of nodes) { 49 | 50 | if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") { 51 | 52 | figma.notify("'"+ node.type + "' node is not supported."); 53 | return; 54 | } 55 | 56 | if (node.x < position.x) { 57 | position.x = node.x; 58 | } 59 | 60 | if (node.y < position.y) { 61 | position.y = node.y; 62 | } 63 | } 64 | 65 | const componentNode = figma.createComponent(); 66 | componentNode.name = nodes[0].name; 67 | componentNode.fills = []; 68 | componentNode.clipsContent = false; 69 | componentNode.constrainProportions = true; 70 | 71 | componentNode.x = position.x; 72 | componentNode.y = position.y; 73 | 74 | componentNode.resize(iconSize, iconSize); 75 | 76 | const vectorNode = figma.flatten(nodes, componentNode); 77 | vectorNode.name = "Vector"; 78 | vectorNode.constraints = { horizontal:"SCALE", vertical:"SCALE" }; 79 | vectorNode.fills = [ figma.util.solidPaint('#FFFFFFFF') ]; 80 | 81 | const ratio = 0.64; 82 | const size = iconSize * ratio; 83 | 84 | const max = Math.max(vectorNode.width, vectorNode.height); 85 | const vectorSizeRatio = size / max; 86 | const vectorWidth = vectorNode.width * vectorSizeRatio; 87 | const vectorHeight = vectorNode.height * vectorSizeRatio; 88 | 89 | vectorNode.resizeWithoutConstraints(vectorWidth, vectorHeight); 90 | vectorNode.x = (iconSize - vectorWidth) * 0.5; 91 | vectorNode.y = (iconSize - vectorHeight) * 0.5; 92 | 93 | figma.currentPage.selection = [ componentNode ]; 94 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { on, showUI } from '@create-figma-plugin/utilities' 2 | import { deserializeMetadata, events, NodeMetadata, pluginData, serializeMetadata, Warning } from './types' 3 | 4 | type NodeWithTransform = GroupNode | FrameNode | ComponentNode | InstanceNode | BooleanOperationNode | VectorNode | LineNode | RectangleNode 5 | 6 | export default async function () { 7 | figma.on('selectionchange', async function () { 8 | await refreshUIAsync(); 9 | }); 10 | 11 | figma.on('currentpagechange', async function () { 12 | await refreshUIAsync(); 13 | }); 14 | 15 | figma.once('close', function () { 16 | removeWarningNodes(); 17 | }); 18 | 19 | on(events.selectedNodeUpdated, async function (metadataStr: string, repaint: boolean) { 20 | const node = getSelectedNode(); 21 | const metadata = deserializeMetadata(metadataStr); 22 | updateNodeByMetadata(node, metadata); 23 | updateBindingKeyForVariants(node); 24 | 25 | if (repaint) { 26 | await refreshUIAsync(); 27 | } 28 | }); 29 | 30 | on(events.selectNode, function (nodeId: string) { 31 | const targetNode = figma.currentPage.findOne(node => node.id === nodeId); 32 | if (targetNode != null) { 33 | figma.currentPage.selection = [targetNode]; 34 | } else { 35 | console.log('Specified node could not found: ' + nodeId); 36 | } 37 | }); 38 | 39 | on(events.showWarnings, async function () { 40 | //console.log('showWarnings'); 41 | const node = getSelectedNode(); 42 | const metadata = await createMetadataFromNodeAsync(node); 43 | 44 | await checkWarningsForNodeAsync(node, metadata); 45 | 46 | removeWarningNodes(); 47 | await drawWarningNodesAsync(metadata.warnings); 48 | }); 49 | 50 | on(events.hideWarnings, function () { 51 | //console.log('hideWarnings'); 52 | removeWarningNodes(); 53 | }); 54 | 55 | on(events.refreshUI, async function () { 56 | await refreshUIAsync(); 57 | }); 58 | 59 | /*on('RESIZE_WINDOW', function (windowSize: { width: number; height: number }) { 60 | const { width, height } = windowSize 61 | figma.ui.resize(width, height) 62 | })*/ 63 | 64 | await refreshUIAsync(); 65 | } 66 | 67 | async function refreshUIAsync(): Promise { 68 | const node = getSelectedNode(); 69 | 70 | var metadataJson: string = ""; 71 | 72 | if (node != null && !isWarningNode(node)) { 73 | removeWarningNodes(); 74 | const metadata = await createMetadataFromNodeAsync(node); 75 | await checkWarningsForNodeAsync(node, metadata); 76 | 77 | metadataJson = serializeMetadata(metadata); 78 | } 79 | 80 | // console.log('Selected node: ' + metadataJson); 81 | 82 | const options = { width: 240, height: 440 }; 83 | showUI(options, { metadataJson: metadataJson }); 84 | } 85 | 86 | function getSelectedNode() { 87 | var node: BaseNode = figma.currentPage.selection[0]; 88 | 89 | if (!node) { 90 | node = figma.currentPage; 91 | } 92 | return node; 93 | } 94 | 95 | function supportsChildren(node: SceneNode | PageNode): 96 | node is FrameNode | ComponentNode | InstanceNode | BooleanOperationNode { 97 | return node.type === 'FRAME' || node.type === 'GROUP' || 98 | node.type === 'COMPONENT' || node.type === 'INSTANCE' || 99 | node.type === 'BOOLEAN_OPERATION' || node.type === 'PAGE'; 100 | } 101 | 102 | type NodeWithChildren = GroupNode | FrameNode | ComponentNode | InstanceNode | BooleanOperationNode | PageNode | ComponentSetNode 103 | 104 | 105 | const warningNodeName = '<<>>'; 106 | 107 | function isWarningNode(node: SceneNode | PageNode): boolean { 108 | if (node.type === 'FRAME' && node.name === warningNodeName) { 109 | return true; 110 | } 111 | 112 | var nodeChildren = node as NodeWithChildren; 113 | if (nodeChildren != null && nodeChildren.parent != null) { 114 | return nodeChildren.parent.type === 'FRAME' && nodeChildren.parent.name === warningNodeName 115 | } 116 | 117 | return false; 118 | } 119 | 120 | function removeWarningNodes() { 121 | var root = figma.currentPage.findOne(node => isWarningNode(node)) as FrameNode; 122 | if (root != null) { 123 | root.remove(); 124 | } 125 | } 126 | 127 | async function drawWarningNodesAsync(warnings: Warning[]): Promise { 128 | var root = figma.createFrame(); 129 | root.name = warningNodeName; 130 | root.clipsContent = false; 131 | root.fills = []; 132 | root.strokes = []; 133 | root.locked = true; 134 | root.expanded = false; 135 | root.resize(1, 1); 136 | 137 | const rootMetadata = await createMetadataFromNodeAsync(root); 138 | rootMetadata.ignored = true; 139 | updateNodeByMetadata(root, rootMetadata); 140 | 141 | figma.currentPage.appendChild(root); 142 | 143 | warnings.forEach(warning => { 144 | var rect = figma.createFrame(); 145 | root.appendChild(rect); 146 | 147 | rect.name = warning.node.name; 148 | 149 | var width: number = 0; 150 | var height: number = 0; 151 | 152 | var layout = warning.node as LayoutMixin; 153 | if (layout != null) { 154 | rect.x = layout.absoluteRenderBounds?.x ?? warning.node.x; 155 | rect.y = layout.absoluteRenderBounds?.y ?? warning.node.y; 156 | 157 | width = layout.absoluteRenderBounds?.width ?? warning.node.width; 158 | height = layout.absoluteRenderBounds?.height ?? warning.node.height; 159 | 160 | } else { 161 | rect.x = warning.node.x; 162 | rect.y = warning.node.y; 163 | width = warning.node.width; 164 | height = warning.node.height; 165 | } 166 | 167 | width = Math.max(1, width); 168 | height = Math.max(1, height); 169 | rect.resize(width, height); 170 | 171 | rect.clipsContent = false; 172 | rect.fills = []; 173 | rect.strokes = [{ type: 'SOLID', color: { r: 1, g: 0, b: 1 } }]; 174 | rect.strokeAlign = 'OUTSIDE'; 175 | rect.strokeWeight = 2; 176 | }); 177 | } 178 | 179 | async function checkWarningsForNodeAsync(node: SceneNode | PageNode, metadata: NodeMetadata): Promise { 180 | if (isWarningNode(node)) { 181 | return; 182 | } 183 | 184 | var currentMetadata = await createMetadataFromNodeAsync(node); 185 | if (currentMetadata.ignored) { 186 | return; 187 | } 188 | 189 | if (node.type !== 'PAGE') { 190 | await checkWarningsAllAsync(node, metadata); 191 | } else { 192 | var children = (node as NodeWithChildren).children; 193 | 194 | for (const child of children) { 195 | await checkWarningsForNodeRecurseAsync(child, metadata); 196 | } 197 | } 198 | } 199 | 200 | async function checkWarningsForNodeRecurseAsync(node: SceneNode, metadata: NodeMetadata): Promise { 201 | if (isWarningNode(node)) { 202 | return; 203 | } 204 | 205 | var currentMetadata = await createMetadataFromNodeAsync(node); 206 | if (currentMetadata.ignored) { 207 | return; 208 | } 209 | 210 | await checkWarningsAllAsync(node, metadata); 211 | 212 | if (supportsChildren(node)) { 213 | var children = (node as NodeWithChildren).children; 214 | 215 | for (const child of children) { 216 | await checkWarningsForNodeRecurseAsync(child, metadata); 217 | } 218 | } 219 | } 220 | 221 | async function checkWarningsAllAsync(node: SceneNode, metadata: NodeMetadata): Promise { 222 | await checkWarningIfMissingComponentReferenceAsync(node, metadata); 223 | checkWarningIfHasRotation(node, metadata); 224 | checkWarningIfMask(node, metadata); 225 | checkWarningIfLine(node, metadata); 226 | } 227 | 228 | async function checkWarningIfMissingComponentReferenceAsync(node: SceneNode, metadata: NodeMetadata): Promise { 229 | if (node.type === 'INSTANCE') { 230 | var instanceNode = node as InstanceNode; 231 | 232 | var mainComponent = await instanceNode.getMainComponentAsync(); 233 | if (mainComponent == null || mainComponent.removed || (!mainComponent.remote && mainComponent.parent == null)) { 234 | metadata.warnings.push(new Warning("Missing component.", node)); 235 | } 236 | } 237 | } 238 | 239 | function checkWarningIfLine(node: SceneNode, metadata: NodeMetadata) { 240 | if (node.type === 'LINE') { 241 | metadata.warnings.push(new Warning("Line does not supported; use 'Outline stroke'.", node)); 242 | } 243 | } 244 | 245 | function checkWarningIfMask(node: SceneNode, metadata: NodeMetadata) { 246 | type NodeWithMask = GroupNode | FrameNode | ComponentNode | InstanceNode | BooleanOperationNode | VectorNode | LineNode | RectangleNode 247 | 248 | const n = node as NodeWithMask; 249 | if (n != null && n.isMask) { 250 | metadata.warnings.push(new Warning("Mask does not supported.", node)); 251 | } 252 | } 253 | 254 | function checkWarningIfHasRotation(node: SceneNode, metadata: NodeMetadata) { 255 | const n = node as NodeWithTransform; 256 | if (n != null && n.rotation > 0.001 || n.rotation < -0.001) { 257 | metadata.warnings.push(new Warning("Rotation does not supported.", node)); 258 | } 259 | } 260 | 261 | async function createMetadataFromNodeAsync(node: BaseNode): Promise { 262 | let metadata = new NodeMetadata(); 263 | metadata.id = node.id; 264 | metadata.type = node.type; 265 | metadata.name = node.name; 266 | metadata.ignored = node.getPluginData(pluginData.ignored).toLowerCase() === 'true'; 267 | metadata.bindingKey = node.getPluginData(pluginData.bindingKey); 268 | metadata.localizationKey = node.getPluginData(pluginData.localizationKey); 269 | metadata.componentType = node.getPluginData(pluginData.componentType); 270 | metadata.tags = node.getPluginData(pluginData.tags); 271 | 272 | if (node.type == 'INSTANCE') { 273 | const instanceNode = node as InstanceNode; 274 | const mainComponent = await instanceNode.getMainComponentAsync(); 275 | 276 | if (mainComponent != null) { 277 | const isComponentSet = mainComponent.parent != null && mainComponent.parent.type == 'COMPONENT_SET'; 278 | 279 | metadata.componentType = isComponentSet ? 280 | mainComponent.parent.getPluginData(pluginData.componentType) : 281 | mainComponent.getPluginData(pluginData.componentType); 282 | } 283 | } 284 | 285 | try { 286 | var componentData = JSON.parse(node.getPluginData(pluginData.componentData)); 287 | metadata.componentData = componentData; 288 | } catch (e) { } 289 | 290 | return metadata; 291 | } 292 | 293 | function updateNodeByMetadata(node: BaseNode, metadata: NodeMetadata | null) { 294 | if (metadata != null) { 295 | node.setPluginData(pluginData.ignored, metadata.ignored ? 'true' : 'false'); 296 | node.setPluginData(pluginData.bindingKey, metadata.bindingKey ? metadata.bindingKey : ''); 297 | node.setPluginData(pluginData.localizationKey, metadata.localizationKey ? metadata.localizationKey : ''); 298 | node.setPluginData(pluginData.tags, metadata.tags ? metadata.tags : ''); 299 | node.setPluginData(pluginData.componentData, metadata.componentData ? JSON.stringify(metadata.componentData) : ''); 300 | 301 | if (node.type == 'COMPONENT_SET' || node.type == 'COMPONENT') { 302 | node.setPluginData(pluginData.componentType, metadata.componentType ? metadata.componentType : ''); 303 | } 304 | 305 | 306 | console.log('Selected node updated: ' + serializeMetadata(metadata)); 307 | } 308 | } 309 | 310 | function updateBindingKeyForVariants(node: BaseNode) { 311 | 312 | updateDataForVariants(node, variant => { 313 | var bindingKey = node.getPluginData(pluginData.bindingKey); 314 | bindingKey = bindingKey ? bindingKey : ''; 315 | variant.setPluginData(pluginData.bindingKey, bindingKey); 316 | 317 | console.log("Component set node binding key updated for: (" + variant.id + "," + variant.name + ")"); 318 | }) 319 | } 320 | 321 | function updateLocalizationKeyForVariants(node: BaseNode) { 322 | if (node.type !== 'TEXT') 323 | return; 324 | 325 | updateDataForVariants(node, variant => { 326 | var localizationKey = node.getPluginData(pluginData.localizationKey); 327 | localizationKey = localizationKey ? localizationKey : ''; 328 | variant.setPluginData(pluginData.localizationKey, localizationKey); 329 | 330 | console.log("Component set node binding key updated for: (" + variant.id + "," + variant.name + ")"); 331 | }) 332 | } 333 | 334 | function updateDataForVariants(node: BaseNode, updateNode: (v: SceneNode) => void) { 335 | const path: number[] = []; 336 | const componentSet = findAttachedComponentSet(node, path); 337 | 338 | if (componentSet != null) { 339 | //console.log("Component set: " + componentSet?.name + " path: " + path); 340 | const variants = componentSet.children; 341 | 342 | 343 | for (let j = 0; j < variants.length; j++) { 344 | let target: NodeWithChildren = variants[j] as NodeWithChildren; 345 | 346 | for (let i = 2; i < path.length; i++) { 347 | if (i == path.length - 1) { 348 | const variant = target.children[path[i]]; 349 | if (variant && variant.type === node.type) { 350 | updateNode(variant); 351 | } 352 | } else { 353 | target = target.children[path[i]] as NodeWithChildren; 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | function findAttachedComponentSet(node: BaseNode, path: number[]): ComponentSetNode | null { 361 | path.push(getSiblingIndex(node)); 362 | 363 | for (let current = node.parent; current != null; current = current.parent) { 364 | 365 | path.push(getSiblingIndex(current)); 366 | 367 | if (current.type === 'COMPONENT_SET') { 368 | path = path.reverse(); 369 | return current; 370 | } 371 | } 372 | 373 | path = []; 374 | return null; 375 | } 376 | 377 | function getSiblingIndex(node: BaseNode): number { 378 | if (node.parent == null) 379 | return -1; 380 | 381 | var parent = node.parent as NodeWithChildren; 382 | if (parent != null) { 383 | for (let i = 0; i < parent.children.length; i++) { 384 | if (parent.children[i] == node) { 385 | return i; 386 | } 387 | } 388 | } 389 | 390 | return -1; 391 | } 392 | -------------------------------------------------------------------------------- /src/ui.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Container, 3 | Columns, 4 | render, 5 | Bold, 6 | Text, 7 | Textbox, 8 | VerticalSpace, 9 | TextboxAutocomplete, 10 | TextboxAutocompleteOption, 11 | TextboxColor, 12 | DropdownOption, 13 | Dropdown, 14 | Button, 15 | Toggle, 16 | Inline, 17 | IconButton, 18 | IconTarget32, 19 | IconSwap32, 20 | IconInfo32, 21 | Banner, 22 | Disclosure, 23 | IconWarning32, 24 | Divider 25 | } from '@create-figma-plugin/ui' 26 | 27 | import { emit } from '@create-figma-plugin/utilities' 28 | import { h, JSX } from 'preact' 29 | import { useState } from 'preact/hooks' 30 | import { deserializeMetadata, events, NodeMetadata, pluginData, serializeMetadata } from './types' 31 | 32 | 33 | let metadata: NodeMetadata | null = null; 34 | 35 | function emitNodeUpdated(repaint: boolean) { 36 | let metadataStr = serializeMetadata(metadata); 37 | emit(events.selectedNodeUpdated, metadataStr, repaint); 38 | } 39 | 40 | enum SliderDirection { 41 | LeftToRight = 'LeftToRight', 42 | RightToLeft = 'RightToLeft', 43 | BottomToTop = 'BottomToTop', 44 | TopToBottom = 'TopToBottom' 45 | } 46 | 47 | enum ProgressDirection { 48 | Horizontal = 'Horizontal', 49 | Vertical = 'Vertical', 50 | Radial90 = 'Radial90', 51 | Radial180 = 'Radial180', 52 | Radial360 = 'Radial360' 53 | } 54 | 55 | enum ProgressBarOriginHorizontal { 56 | Left = 'Left', 57 | Right = 'Right' 58 | } 59 | 60 | enum ProgressBarOriginVertical { 61 | Bottom = 'Bottom', 62 | Top = 'Top' 63 | } 64 | 65 | enum ProgressBarOriginRadial90 { 66 | BottomLeft = 'BottomLeft', 67 | TopLeft = 'TopLeft', 68 | TopRight = 'TopRight', 69 | BottomRight = 'BottomRight' 70 | } 71 | 72 | enum ProgressBarOriginRadial180 { 73 | Bottom = 'Bottom', 74 | Top = 'Top', 75 | Left = 'Left', 76 | Right = 'Right' 77 | } 78 | 79 | enum ProgressBarOriginRadial360 { 80 | Bottom = 'Bottom', 81 | Top = 'Top', 82 | Left = 'Left', 83 | Right = 'Right' 84 | } 85 | 86 | export abstract class ComponentData { 87 | 88 | public updateData() 89 | { 90 | if (metadata != null) { 91 | if (metadata.componentData != null) { 92 | Object.assign(this, metadata.componentData); 93 | } 94 | 95 | metadata.componentData = this; 96 | } 97 | } 98 | 99 | abstract getType(): string; 100 | abstract getForm(): h.JSX.Element | null; 101 | } 102 | 103 | export class SelectableData extends ComponentData { 104 | getType(): string { 105 | return 'Selectable'; 106 | } 107 | 108 | getForm(): h.JSX.Element | null { 109 | return null; 110 | } 111 | } 112 | 113 | export class ButtonData extends ComponentData { 114 | getType(): string { 115 | return 'Button'; 116 | } 117 | 118 | getForm(): h.JSX.Element | null { 119 | return null; 120 | } 121 | } 122 | 123 | class ToggleData extends ComponentData { 124 | getType(): string { 125 | return 'Toggle'; 126 | } 127 | 128 | getForm(): h.JSX.Element | null { 129 | return null; 130 | } 131 | } 132 | 133 | class ProgressBarData extends ComponentData { 134 | public direction: ProgressDirection = ProgressDirection.Horizontal; 135 | public originHorizontal: ProgressBarOriginHorizontal = ProgressBarOriginHorizontal.Left; 136 | public originVertical: ProgressBarOriginVertical = ProgressBarOriginVertical.Bottom; 137 | public originRadial90: ProgressBarOriginRadial90 = ProgressBarOriginRadial90.BottomLeft; 138 | public originRadial180: ProgressBarOriginRadial180 = ProgressBarOriginRadial180.Bottom; 139 | public originRadial360: ProgressBarOriginRadial360 = ProgressBarOriginRadial360.Bottom; 140 | public clockwise: boolean = false; 141 | 142 | getType(): string { 143 | return 'ProgressBar'; 144 | } 145 | 146 | getForm(): h.JSX.Element | null { 147 | const options: Array = Object.values(ProgressDirection).map(v => ({ value: v })); 148 | const [direction, setDirection] = useState(this.direction); 149 | 150 | const originHorizontalOptions: Array = Object.values(ProgressBarOriginHorizontal).map(v => ({ value : v})); 151 | const [originHorizontal, setHorizontalOrigin] = useState(this.originHorizontal); 152 | 153 | const originVerticalOptions: Array = Object.values(ProgressBarOriginVertical).map(v => ({ value : v})); 154 | const [originVertical, setVerticalOrigin] = useState(this.originVertical); 155 | 156 | const originRadial90Options: Array = Object.values(ProgressBarOriginRadial90).map(v => ({ value : v})); 157 | const [originRadial90, setRadial90Origin] = useState(this.originRadial90); 158 | 159 | const originRadial180Options: Array = Object.values(ProgressBarOriginRadial180).map(v => ({ value : v})); 160 | const [originRadial180, setRadial180Origin] = useState(this.originRadial180); 161 | 162 | const originRadial360Options: Array = Object.values(ProgressBarOriginRadial360).map(v => ({ value : v})); 163 | const [originRadial360, setRadial360Origin] = useState(this.originRadial360); 164 | 165 | const [clockwise, setClockwise] = useState(this.clockwise); 166 | 167 | function getProgressBar() { 168 | return metadata?.componentData as ProgressBarData; 169 | } 170 | 171 | function handleDirectionInput(event: JSX.TargetedEvent) { 172 | const value = event.currentTarget.value as ProgressDirection; 173 | setDirection(value); 174 | getProgressBar().direction = value; 175 | emitNodeUpdated(true); 176 | } 177 | 178 | function handleClockwise(event: JSX.TargetedEvent) { 179 | const checked = event.currentTarget.checked; 180 | setClockwise(checked); 181 | getProgressBar().clockwise = checked; 182 | emitNodeUpdated(false); 183 | } 184 | 185 | function handleOriginHorizontalInput(event: JSX.TargetedEvent) { 186 | const value = event.currentTarget.value as ProgressBarOriginHorizontal; 187 | setHorizontalOrigin(value); 188 | getProgressBar().originHorizontal = value; 189 | emitNodeUpdated(false); 190 | } 191 | 192 | function handleOriginVerticalInput(event: JSX.TargetedEvent) { 193 | const value = event.currentTarget.value as ProgressBarOriginVertical; 194 | setVerticalOrigin(value); 195 | getProgressBar().originVertical = value; 196 | emitNodeUpdated(false); 197 | } 198 | 199 | function handleOriginRadial90Input(event: JSX.TargetedEvent) { 200 | const value = event.currentTarget.value as ProgressBarOriginRadial90; 201 | setRadial90Origin(value); 202 | getProgressBar().originRadial90 = value; 203 | emitNodeUpdated(false); 204 | } 205 | 206 | function handleOriginRadial180Input(event: JSX.TargetedEvent) { 207 | const value = event.currentTarget.value as ProgressBarOriginRadial180; 208 | setRadial180Origin(value); 209 | getProgressBar().originRadial180 = value; 210 | emitNodeUpdated(false); 211 | } 212 | 213 | function handleOriginRadial360Input(event: JSX.TargetedEvent) { 214 | const value = event.currentTarget.value as ProgressBarOriginRadial360; 215 | setRadial360Origin(value); 216 | getProgressBar().originRadial360 = value; 217 | emitNodeUpdated(false); 218 | } 219 | 220 | const layout: Array = []; 221 | 222 | layout.push( 223 | 224 | 225 | Direction 226 | 227 | 228 | 229 | 230 | ); 231 | 232 | if (direction == ProgressDirection.Horizontal) { 233 | layout.push( 234 | 235 | 236 | Origin 237 | 238 | 239 | 240 | 241 | ); 242 | } else if (direction == ProgressDirection.Vertical) { 243 | layout.push( 244 | 245 | 246 | Origin 247 | 248 | 249 | 250 | 251 | ); 252 | } else if (direction == ProgressDirection.Radial90) { 253 | layout.push( 254 | 255 | 256 | Origin 257 | 258 | 259 | 260 | 261 | ); 262 | } else if (direction == ProgressDirection.Radial180) { 263 | layout.push( 264 | 265 | 266 | Origin 267 | 268 | 269 | 270 | 271 | ); 272 | } else if (direction == ProgressDirection.Radial360) { 273 | layout.push( 274 | 275 | 276 | Origin 277 | 278 | 279 | 280 | 281 | ); 282 | } 283 | 284 | if (direction == ProgressDirection.Radial90 || 285 | direction == ProgressDirection.Radial180 || 286 | direction == ProgressDirection.Radial360) { 287 | layout.push( 288 | 289 | 290 | 291 | Clockwise 292 | 293 | 294 | 295 | ); 296 | } 297 | 298 | return ({layout}); 299 | } 300 | } 301 | 302 | class SliderData extends ComponentData { 303 | public direction: SliderDirection = SliderDirection.LeftToRight; 304 | 305 | getType(): string { 306 | return 'Slider'; 307 | } 308 | 309 | getForm(): h.JSX.Element | null { 310 | const options: Array = Object.values(SliderDirection).map(v => ({ value: v })); 311 | const [direction, setDirection] = useState(this.direction); 312 | 313 | function getSlider() { 314 | return metadata?.componentData as SliderData; 315 | } 316 | 317 | function handleDirectionInput(event: JSX.TargetedEvent) { 318 | const value = event.currentTarget.value as SliderDirection; 319 | setDirection(value); 320 | getSlider().direction = value; 321 | emitNodeUpdated(false); 322 | } 323 | 324 | return ( 325 | 326 | 327 | Direction 328 | 329 | 330 | 331 | 332 | ) 333 | } 334 | } 335 | 336 | class ScrollbarData extends ComponentData { 337 | public direction: SliderDirection = SliderDirection.LeftToRight; 338 | 339 | getType(): string { 340 | return 'Scrollbar'; 341 | } 342 | 343 | getForm(): h.JSX.Element | null { 344 | const options: Array = Object.values(SliderDirection).map(v => ({ value: v })); 345 | const [direction, setDirection] = useState(this.direction); 346 | 347 | function getScrollbar() { 348 | return metadata?.componentData as ScrollbarData; 349 | } 350 | 351 | function handleDirectionInput(event: JSX.TargetedEvent) { 352 | const value = event.currentTarget.value as SliderDirection; 353 | setDirection(value); 354 | getScrollbar().direction = value; 355 | emitNodeUpdated(false); 356 | } 357 | 358 | return ( 359 | 360 | 361 | Direction 362 | 363 | 364 | 365 | 366 | ) 367 | } 368 | } 369 | 370 | enum ScrollViewVisibility { 371 | Permanent = 'Permanent', 372 | AutoHide = 'AutoHide', 373 | AutoHideAndExpandViewport = 'AutoHideAndExpandViewport' 374 | } 375 | 376 | class ScrollViewData extends ComponentData { 377 | public horizontalVisibility: ScrollViewVisibility = ScrollViewVisibility.AutoHideAndExpandViewport; 378 | public verticalVisibility: ScrollViewVisibility = ScrollViewVisibility.AutoHideAndExpandViewport; 379 | 380 | getType(): string { 381 | return 'ScrollView'; 382 | } 383 | 384 | getForm(): h.JSX.Element | null { 385 | const options: Array = Object.values(ScrollViewVisibility).map(v => ({ value: v })); 386 | const [horizontalVisibility, setHorizontalVisibility] = useState(this.horizontalVisibility); 387 | const [verticalVisibility, setVerticalVisibility] = useState(this.verticalVisibility); 388 | 389 | function getScrollView() { 390 | return metadata?.componentData as ScrollViewData; 391 | } 392 | 393 | function handleHorizontalVisibilityInput(event: JSX.TargetedEvent) { 394 | const value = event.currentTarget.value as ScrollViewVisibility; 395 | setHorizontalVisibility(value); 396 | getScrollView().horizontalVisibility = value; 397 | emitNodeUpdated(false); 398 | } 399 | 400 | function handleVerticalVisibilityInput(event: JSX.TargetedEvent) { 401 | const value = event.currentTarget.value as ScrollViewVisibility; 402 | setVerticalVisibility(value); 403 | getScrollView().verticalVisibility = value; 404 | emitNodeUpdated(false); 405 | } 406 | 407 | return ( 408 | 409 | 410 | Horizontal Scrollbar Visibility 411 | 412 | 413 | 414 | 415 | 416 | Vertical Scrollbar Visibility 417 | 418 | 419 | 420 | 421 | ) 422 | } 423 | } 424 | 425 | class DropdownData extends ComponentData { 426 | getType(): string { 427 | return 'Dropdown'; 428 | } 429 | 430 | getForm(): h.JSX.Element | null { 431 | return null; 432 | } 433 | } 434 | 435 | class InputFieldData extends ComponentData { 436 | public selectionColor: string = 'A8CEFF'; 437 | public selectionColorOpacity: string = '75'; 438 | public caretColor: string = '323232'; 439 | public caretColorOpacity: string = '100'; 440 | public caretWidth: number = 1; 441 | 442 | getType(): string { 443 | return 'InputField'; 444 | } 445 | 446 | getForm(): h.JSX.Element | null { 447 | const [selectionColor, setSelectionColor] = useState(this.selectionColor); 448 | const [selectionColorOpacity, setSelectionColorOpacity] = useState(this.selectionColorOpacity); 449 | 450 | const [caretColor, setCaretColor] = useState(this.caretColor); 451 | const [caretColorOpacity, setCaretColorOpacity] = useState(this.caretColorOpacity); 452 | const [caretWidth, setCaretWidth] = useState(this.caretWidth); 453 | 454 | const caretWidths: Array = [ 455 | {value: "1"}, 456 | {value: "2"}, 457 | {value: "3"}, 458 | {value: "4"}, 459 | {value: "5"} 460 | ]; 461 | 462 | function getInputField() { 463 | return metadata?.componentData as InputFieldData; 464 | } 465 | 466 | function handleSelectionColorInput(event: JSX.TargetedEvent) { 467 | setSelectionColor(event.currentTarget.value); 468 | getInputField().selectionColor = event.currentTarget.value; 469 | emitNodeUpdated(false); 470 | } 471 | 472 | function handleSelectionColorOpacityInput(event: JSX.TargetedEvent) { 473 | setSelectionColorOpacity(event.currentTarget.value); 474 | getInputField().selectionColorOpacity = event.currentTarget.value; 475 | emitNodeUpdated(false); 476 | } 477 | 478 | function handleCaretColorInput(event: JSX.TargetedEvent) { 479 | setCaretColor(event.currentTarget.value); 480 | getInputField().caretColor = event.currentTarget.value; 481 | emitNodeUpdated(false); 482 | } 483 | 484 | function handleCaretColorOpacityInput(event: JSX.TargetedEvent) { 485 | setCaretColorOpacity(event.currentTarget.value); 486 | getInputField().caretColorOpacity = event.currentTarget.value; 487 | emitNodeUpdated(false); 488 | } 489 | 490 | function handleCaretWidthInput(value: string) { 491 | const valueInt = parseInt(value); 492 | 493 | if (!isNaN(valueInt)) { 494 | setCaretWidth(valueInt); 495 | getInputField().caretWidth = valueInt; 496 | emitNodeUpdated(false); 497 | } 498 | } 499 | 500 | return ( 501 | 502 | 503 | Selection Color 504 | 505 | 506 | 507 | 508 | 509 | Caret Color 510 | 511 | 512 | 513 | 514 | 515 | Caret Width 516 | 517 | 518 | 519 | ) 520 | } 521 | } 522 | 523 | function drawTitleField(): h.JSX.Element | null { 524 | if (metadata != null) 525 | { 526 | return ( 527 | 528 | 529 | {metadata.name} ({metadata.type}) 530 | 531 | 532 | ) 533 | } 534 | 535 | return null; 536 | } 537 | 538 | function drawIgnoredField(): h.JSX.Element | null { 539 | const [ignored, setIgnored] = useState(metadata != null ? metadata.ignored : false); 540 | 541 | function handleChange(event: JSX.TargetedEvent) { 542 | if (metadata != null) 543 | { 544 | metadata.ignored = event.currentTarget.checked; 545 | setIgnored(metadata.ignored); 546 | emitNodeUpdated(true); 547 | } 548 | } 549 | 550 | return ( 551 | 552 | 553 | 554 | Ignored 555 | 556 | 557 | 558 | ) 559 | } 560 | 561 | function drawBindingKeyField(): h.JSX.Element | null { 562 | const [bindingKey, setBindingKey] = useState(metadata != null ? metadata.bindingKey : ''); 563 | 564 | function handleInput(event: JSX.TargetedEvent) { 565 | if (metadata != null) 566 | { 567 | metadata.bindingKey = event.currentTarget.value; 568 | setBindingKey(metadata.bindingKey); 569 | emitNodeUpdated(false); 570 | } 571 | } 572 | 573 | return ( 574 | 575 | 576 | Binding Key 577 | 578 | 579 | 580 | 581 | ) 582 | } 583 | 584 | function drawLocalizationKeyField(): h.JSX.Element | null { 585 | const [localizationKey, setLocalizationKey] = useState(metadata != null ? metadata.localizationKey : ''); 586 | 587 | function handleInput(event: JSX.TargetedEvent) { 588 | if (metadata != null) 589 | { 590 | metadata.localizationKey = event.currentTarget.value; 591 | setLocalizationKey(metadata.localizationKey); 592 | emitNodeUpdated(false); 593 | } 594 | } 595 | 596 | const isText: boolean = metadata == null ? false : metadata.type == 'TEXT'; 597 | if (isText) 598 | { 599 | return ( 600 | 601 | 602 | Localization Key 603 | 604 | 605 | 606 | 607 | ) 608 | } 609 | 610 | return null; 611 | } 612 | 613 | function drawTagsField(): h.JSX.Element | null { 614 | const [tags, setTags] = useState(metadata != null ? metadata.tags : ''); 615 | 616 | function handleInput(event: JSX.TargetedEvent) { 617 | if (metadata != null) 618 | { 619 | metadata.tags = event.currentTarget.value; 620 | setTags(metadata.tags); 621 | emitNodeUpdated(false); 622 | } 623 | } 624 | 625 | return ( 626 | 627 | 628 | Tags 629 | 630 | 631 | 632 | 633 | ); 634 | } 635 | 636 | function drawExtraControls(): h.JSX.Element | null { 637 | 638 | return null; 639 | } 640 | 641 | function drawWarningsField(): h.JSX.Element | null { 642 | const [expand, setExpand] = useState(false); 643 | 644 | function handleClick(event: JSX.TargetedMouseEvent) { 645 | if (!(expand === true)) { 646 | setExpand(true); 647 | emit(events.showWarnings); 648 | } else { 649 | setExpand(false); 650 | emit(events.hideWarnings); 651 | } 652 | } 653 | 654 | function goToNode(nodeId: string) { 655 | emit(events.selectNode, nodeId); 656 | } 657 | 658 | if (metadata != null && metadata.warnings.length > 0) 659 | { 660 | const layout: Array = []; 661 | 662 | if (metadata.type === 'PAGE') { 663 | metadata.warnings.forEach(warning => { 664 | layout.push( goToNode(warning.nodeId)} title="Select element">{warning.nodeName}); 665 | }); 666 | 667 | const style = { height: '64px' } 668 | return ( 669 |
670 | 671 | {layout} 672 | 673 |
674 | ) 675 | } 676 | 677 | metadata.warnings.forEach(warning => { 678 | layout.push(
{warning.message}
); 679 | }); 680 | 681 | return (} variant="warning">{layout}) 682 | } 683 | 684 | return null; 685 | } 686 | 687 | function drawComponentTypeField() : h.JSX.Element | null { 688 | const [componentType, setComponentType] = useState(metadata != null ? metadata.componentType : ''); 689 | 690 | const options: Array = []; 691 | 692 | const components: Array = [ 693 | new SelectableData(), 694 | new ButtonData(), 695 | new ToggleData(), 696 | new InputFieldData(), 697 | new DropdownData(), 698 | new SliderData(), 699 | new ProgressBarData(), 700 | new ScrollViewData(), 701 | new ScrollbarData(), 702 | ]; 703 | 704 | components.forEach(component => { 705 | options.push({ value: component.getType() }); 706 | }); 707 | 708 | 709 | const isComponent: boolean = metadata == null ? false : metadata.type == 'COMPONENT'; 710 | const isComponentSet: boolean = metadata == null ? false : metadata.type == 'COMPONENT_SET'; 711 | const isInstance: boolean = metadata == null ? false : metadata.type == 'INSTANCE'; 712 | 713 | let componentForm: JSX.Element | null = null; 714 | 715 | if (isComponent || isComponentSet) 716 | { 717 | var component = components.find(x => x.getType() == metadata?.componentType); 718 | if (component) 719 | { 720 | component.updateData(); 721 | componentForm = component.getForm(); 722 | } 723 | } 724 | 725 | function handleInput(event: JSX.TargetedEvent) { 726 | if (metadata != null) 727 | { 728 | metadata.componentType = event.currentTarget.value; 729 | setComponentType(metadata.componentType); 730 | 731 | const isRepaintNeeded = components.findIndex(x => x.getType() == metadata?.componentType) >= 0; 732 | emitNodeUpdated(isRepaintNeeded); 733 | } 734 | } 735 | 736 | function clearComponentData(event: JSX.TargetedMouseEvent) { 737 | if (metadata != null) { 738 | metadata.componentData = null; 739 | emitNodeUpdated(true); 740 | } 741 | } 742 | 743 | if (isComponentSet || isComponent) 744 | { 745 | return ( 746 | 747 | 748 | Component Type 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | {componentForm != null ? componentForm: null} 758 | 759 | ) 760 | } 761 | else if (isInstance) 762 | { 763 | return ( 764 | 765 | 766 | Component Type 767 | 768 | 769 | 770 | 771 | ); 772 | } 773 | 774 | return null; 775 | } 776 | 777 | function Plugin(data: { metadataJson: string} ) { 778 | 779 | const layout: Array = []; 780 | metadata = deserializeMetadata(data.metadataJson); 781 | 782 | //console.log('Node: ' + data.metadataJson); 783 | 784 | if (metadata != null) 785 | { 786 | var titleField = drawTitleField(); 787 | if (titleField != null) { 788 | layout.push(titleField); 789 | } 790 | 791 | var bindingKeyField = drawBindingKeyField(); 792 | if (bindingKeyField != null) { 793 | layout.push(bindingKeyField); 794 | } 795 | 796 | var localizationKeyField = drawLocalizationKeyField(); 797 | if (localizationKeyField != null) { 798 | layout.push(localizationKeyField); 799 | } 800 | 801 | var componenTypeField = drawComponentTypeField(); 802 | if (componenTypeField != null) { 803 | layout.push(componenTypeField); 804 | } 805 | 806 | var tagsField = drawTagsField(); 807 | if (tagsField != null) { 808 | layout.push(tagsField); 809 | } 810 | 811 | var ignoredField = drawIgnoredField(); 812 | if (ignoredField != null) { 813 | layout.push(ignoredField); 814 | } 815 | 816 | var warningsField = drawWarningsField(); 817 | if (warningsField != null) { 818 | layout.push(warningsField); 819 | } 820 | } else { 821 | layout.push( 822 | 823 | 824 | }>Select a node. 825 | 826 | 827 | ); 828 | } 829 | 830 | /*function onWindowResize(windowSize: { width: number; height: number }) { 831 | emit('RESIZE_WINDOW', windowSize) 832 | } 833 | 834 | useWindowResize(onWindowResize, { 835 | minWidth: 240, 836 | minHeight: 240, 837 | maxWidth: 320, 838 | maxHeight: 500 839 | })*/ 840 | 841 | var extraControls = drawExtraControls(); 842 | if (extraControls != null) { 843 | layout.push(); 844 | layout.push(); 845 | layout.push(extraControls); 846 | } 847 | 848 | return ({layout}) 849 | } 850 | 851 | export default render(Plugin); 852 | 853 | --------------------------------------------------------------------------------