├── LICENSE ├── README.md ├── banner_2048x1024.png ├── code.js ├── code.ts ├── figma.d.ts ├── icon_128.png ├── manifest.json └── tsconfig.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Renan Cammarosano 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 | ![logo](https://github.com/renancamm/figma-reattache-instance/blob/master/banner_2048x1024.png?raw=true) 2 | 3 | [![Figma Plugin](https://img.shields.io/badge/Figma_Plugin-%235551ff?logo=figma&logoColor=white)](https://www.figma.com/community/plugin/741415678427267506) 4 | [![Plugin installs](https://img.shields.io/badge/dynamic/json?color=%235551ff&label=Installs&query=%24.meta.plugin.install_count&url=https%3A%2F%2Fwww.figma.com%2Fapi%2Fplugins%2F741415678427267506%2Fversions 5 | )](https://www.figma.com/community/plugin/741415678427267506) 6 | [![Plugin likes](https://img.shields.io/badge/dynamic/json?color=%235551ff&label=Likes&query=%24.meta.plugin.like_count&url=https%3A%2F%2Fwww.figma.com%2Fapi%2Fplugins%2F741415678427267506%2Fversions)](https://www.figma.com/community/plugin/741415678427267506) 7 | 8 | 9 | 10 | 11 | # Reattach Instance (Figma Plugin) 12 | Relink a frame to a component by searching for similar instances. 13 | 14 | ## Use cases: 15 | - Copying a nested component 16 | - Moving the master component to a different file 17 | - Turning a set of similar frames into a component 18 | - Turning detached instances back into instances 19 | 20 | Get it here: https://www.figma.com/c/plugin/741415678427267506/Reattach-Instance 21 | 22 | 23 | ## How it works: 24 | 1. Select frames (that used to be instances) 25 | 2. Click "Plugin/Reattach instance" 26 | 3. Plugin finds instances or master components with the same name 27 | 4. New instance is created for each corresponding frame 28 | 5. The selected frames are replaced by instances 🎉 29 | 30 | 👉 *NEW:* Overrides will now be applied to instances! 31 | 32 | ## Known problems: 33 | - Relies too much on layer naming 34 | - Needs to have an instance/master as a reference on the same file 35 | - Doesn't work all the time with team libraries 36 | 37 | Feel free to contribute 😅 38 | 39 | 40 | --- 41 | 42 | Contributors: 43 | - [mikeozornin](https://github.com/mikeozornin) 44 | - [bespoyasov](https://github.com/bespoyasov) 45 | - [zyumbik](https://twitter.com/zyumbik) 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /banner_2048x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renancamm/figma-reattache-instance/3b4b1a8402397e788ecd87a9c0806e78c65c038b/banner_2048x1024.png -------------------------------------------------------------------------------- /code.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | return new (P || (P = Promise))(function (resolve, reject) { 3 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 4 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 5 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 6 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 7 | }); 8 | }; 9 | function main() { 10 | return __awaiter(this, void 0, void 0, function* () { 11 | if (figma.currentPage.selection.length === 0) { 12 | return "Select frames you want to replace with instances"; 13 | } 14 | let skippedCount = 0; 15 | let processedCount = 0; 16 | let originalInstances = {}; // cache found instances 17 | const clonedSelection = Object.assign([], figma.currentPage.selection); 18 | for (let index in clonedSelection) { 19 | if (clonedSelection[index].type !== "FRAME") { 20 | skippedCount += 1; 21 | continue; 22 | } 23 | const frame = clonedSelection[index]; 24 | let componentReference = null; 25 | if (!(frame.name in originalInstances)) { 26 | // Try to find an instance or master for the frame 27 | componentReference = figma.root.findOne(node => isEquivalentNode(frame, node)); 28 | originalInstances[frame.name] = componentReference; 29 | } 30 | else { 31 | componentReference = originalInstances[frame.name]; 32 | } 33 | // If instance was found, replace frame with it 34 | if (componentReference !== null) { 35 | let instanceClone; 36 | if (componentReference.type === "INSTANCE") { 37 | instanceClone = componentReference.masterComponent.createInstance(); 38 | } 39 | else { 40 | instanceClone = componentReference.createInstance(); 41 | } 42 | // Insert instance right above the frame 43 | let frameIndex = frame.parent.children.indexOf(frame); 44 | frame.parent.insertChild(frameIndex + 1, instanceClone); 45 | // Position and resize new instance to frame 46 | instanceClone.x = frame.x; 47 | instanceClone.y = frame.y; 48 | instanceClone.resize(frame.width, frame.height); 49 | yield overrideProperties(frame, instanceClone); 50 | frame.remove(); 51 | processedCount += 1; 52 | continue; 53 | } 54 | skippedCount += 1; 55 | continue; 56 | } 57 | return `${processedCount} processed, ${skippedCount} skipped`; 58 | }); 59 | } 60 | // Check if node is a component that can replace an instance 61 | function isEquivalentNode(frame, node) { 62 | if (node.type !== "INSTANCE" && node.type !== "COMPONENT") 63 | return false; 64 | if (node.name !== frame.name) 65 | return false; 66 | return true; 67 | } 68 | // Recursively overrides all properties on an instance 69 | function overrideProperties(source, target) { 70 | return __awaiter(this, void 0, void 0, function* () { 71 | // If source is text, the long and tedious process 72 | // of replacing all text properties begins! 73 | if (source.type === "TEXT" && target.type === "TEXT") { 74 | yield overrideTextProperties(source, target); 75 | } 76 | // Try to override every other property 77 | // if it's present in source and not mixed 78 | allProperties.forEach(prop => { 79 | if (!(prop in source)) 80 | return; 81 | if (source[prop] === undefined) 82 | return; 83 | if (areEquivalent(target[prop], source[prop]) || isMixed(source[prop])) 84 | return; 85 | try { 86 | target[prop] = source[prop]; 87 | } 88 | catch (e) { 89 | console.error(e); 90 | } 91 | }); 92 | // Instances can be overriden too 93 | if (source.type === "INSTANCE" && target.type === "INSTANCE") { 94 | if (target.masterComponent.id !== source.masterComponent.id) { 95 | target.masterComponent = source.masterComponent; 96 | } 97 | } 98 | // Recursively change all children 99 | if (supportsChildren(source) && supportsChildren(target)) { 100 | for (let i = 0; i < target.children.length; i++) { 101 | const sourceChild = source.children[i]; 102 | const targetChild = target.children[i]; 103 | if (!sourceChild || !targetChild) 104 | continue; 105 | yield overrideProperties(sourceChild, targetChild); 106 | } 107 | } 108 | }); 109 | } 110 | // Override all properties of text layers 111 | // Mixed properties are applied on each character because there are no alternatives 112 | function overrideTextProperties(source, target) { 113 | return __awaiter(this, void 0, void 0, function* () { 114 | // Collect mixed properties to replace them later 115 | const mixedProperties = []; 116 | const isStringSame = target.characters === source.characters; 117 | // Hacky methods to get character setter/getter methods 118 | // from property names 119 | const getPropertyAtChar = (prop, char) => { 120 | const rangeMethod = "getRange" + prop[0].toUpperCase() + prop.slice(1); 121 | return source[rangeMethod](char, char + 1); 122 | }; 123 | const setPropertyAtChar = (prop, char, value) => { 124 | const rangeMethod = "setRange" + prop[0].toUpperCase() + prop.slice(1); 125 | target[rangeMethod](char, char + 1, value); 126 | }; 127 | // Start off by checking if characters are the same and changing them 128 | const fontName = source.getRangeFontName(0, 1); 129 | yield figma.loadFontAsync(fontName).then(() => { 130 | // With font loaded we reset all properties to 131 | // ones applied to the first character of source 132 | textProperties.forEach(prop => { 133 | if (isMixed(source[prop])) { 134 | mixedProperties.push(prop); 135 | } 136 | if (isStringSame) 137 | return; 138 | const value = getPropertyAtChar(prop, 0); 139 | if (areEquivalent(target[prop], value) 140 | && value !== undefined) 141 | return; 142 | target[prop] = value; 143 | }); 144 | if (!isStringSame) { 145 | target.characters = source.characters; 146 | } 147 | }).catch((e) => { console.error(e); }); 148 | // All properties are set on the whole text without mixed ones! 149 | if (mixedProperties.length === 0) { 150 | return; 151 | } 152 | // Don't override too long texts with many different properties 153 | // because it would take too much time 154 | if (target.characters.length > 150 155 | && mixedProperties.length > 2) { 156 | return; 157 | } 158 | // Load all fonts on the text block 159 | for (let char = 0; char < target.characters.length; char++) { 160 | const fontName = source.getRangeFontName(char, char + 1); 161 | yield figma.loadFontAsync(fontName).catch((e) => { console.error(e); }); 162 | } 163 | // Set properties character by character on mixed fields 164 | // This can't be done in the previous loop 165 | for (let char = 0; char < target.characters.length; char++) { 166 | for (let i = 0; i < mixedProperties.length; i++) { 167 | const prop = mixedProperties[i]; 168 | try { 169 | const value = getPropertyAtChar(prop, char); 170 | if (value === null) 171 | continue; 172 | setPropertyAtChar(prop, char, value); 173 | } 174 | catch (e) { 175 | console.error(e); 176 | } 177 | } 178 | } 179 | }); 180 | } 181 | // Returns type predicate and true if node supports children 182 | function supportsChildren(node) { 183 | return node.type === "FRAME" || node.type === "COMPONENT" || 184 | node.type === "INSTANCE" || node.type === "BOOLEAN_OPERATION"; 185 | } 186 | function isMixed(property) { 187 | return property === figma.mixed && typeof property === "symbol"; 188 | } 189 | // Checks if two objects are equivalent 190 | function areEquivalent(a, b) { 191 | if (a === b) { 192 | return true; 193 | } 194 | if (!(a instanceof Object || b instanceof Object)) { 195 | return false; 196 | } 197 | if (toType(a) !== toType(b)) { 198 | return false; 199 | } 200 | if (Array.isArray(a) && Array.isArray(b)) { 201 | if (a.length !== b.length) { 202 | return false; 203 | } 204 | if (a.length === b.length) { 205 | for (let i = 0; i < a.length; i++) { 206 | if (!areEquivalent(a[i], b[i])) { 207 | return false; 208 | } 209 | } 210 | } 211 | return true; 212 | } 213 | // Create arrays of property names 214 | let aProps = Object.getOwnPropertyNames(a); 215 | let bProps = Object.getOwnPropertyNames(b); 216 | // If number of properties is different, 217 | // objects are not equivalent 218 | if (aProps.length !== bProps.length) { 219 | return false; 220 | } 221 | for (let i = 0; i < aProps.length; i++) { 222 | let prop = aProps[i]; 223 | // Recursion: if values of the same property 224 | // are not equal, objects are not equivalent 225 | if (!areEquivalent(a[prop], b[prop])) { 226 | return false; 227 | } 228 | } 229 | // If we made it this far, objects 230 | // are considered equivalent 231 | return true; 232 | } 233 | // Returns type of an object 234 | function toType(obj) { 235 | return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); 236 | } 237 | // A list of all overrrideable properties in Figma 238 | const allProperties = [ 239 | "visible", 240 | "locked", 241 | "opacity", 242 | "blendMode", 243 | "effects", 244 | "effectStyleId", 245 | // frame 246 | "backgrounds", 247 | "layoutGrids", 248 | "clipsContent", 249 | "guides", 250 | "gridStyleId", 251 | "backgroundStyleId", 252 | // geometry 253 | "fills", 254 | "strokes", 255 | "strokes", 256 | "strokeAlign", 257 | "strokeCap", 258 | "strokeJoin", 259 | "dashPattern", 260 | "fillStyleId", 261 | "strokeStyleId", 262 | "cornerRadius", 263 | "cornerSmoothing", 264 | "exportSettings", 265 | // component instance 266 | "masterComponent", 267 | // text 268 | "autoRename", 269 | "textAlignHorizontal", 270 | "textAlignVertical", 271 | "paragraphIndent", 272 | "paragraphSpacing", 273 | ]; 274 | // Text properties that can be overriden on individual characters 275 | const textProperties = [ 276 | "fills", 277 | "fillStyleId", 278 | "fontSize", 279 | "fontName", 280 | "textCase", 281 | "textDecoration", 282 | "letterSpacing", 283 | "lineHeight", 284 | "textStyleId", 285 | ]; 286 | // Methods are async, close plugin when they are resolved 287 | main().then(msg => { 288 | figma.closePlugin(msg); 289 | }); 290 | -------------------------------------------------------------------------------- /code.ts: -------------------------------------------------------------------------------- 1 | type MasterOrInstance = InstanceNode | ComponentNode; 2 | 3 | async function main() { 4 | 5 | if (figma.currentPage.selection.length === 0) { 6 | return "Select frames you want to replace with instances"; 7 | } 8 | 9 | let skippedCount = 0; 10 | let processedCount = 0; 11 | let originalInstances = {}; // cache found instances 12 | 13 | const clonedSelection = Object.assign([], figma.currentPage.selection); 14 | 15 | for (let index in clonedSelection) { 16 | 17 | if (clonedSelection[index].type !== "FRAME") { 18 | skippedCount += 1; 19 | continue; 20 | } 21 | 22 | const frame = clonedSelection[index] as FrameNode; 23 | 24 | let componentReference: MasterOrInstance = null; 25 | if (!(frame.name in originalInstances)) { 26 | // Try to find an instance or master for the frame 27 | componentReference = figma.root.findOne(node => isEquivalentNode(frame, node)) as MasterOrInstance; 28 | originalInstances[frame.name] = componentReference; 29 | } else { 30 | componentReference = originalInstances[frame.name]; 31 | } 32 | 33 | // If instance was found, replace frame with it 34 | if (componentReference !== null) { 35 | let instanceClone: InstanceNode; 36 | if (componentReference.type === "INSTANCE") { 37 | instanceClone = componentReference.masterComponent.createInstance(); 38 | } else { 39 | instanceClone = componentReference.createInstance(); 40 | } 41 | // Insert instance right above the frame 42 | let frameIndex = frame.parent.children.indexOf(frame); 43 | frame.parent.insertChild(frameIndex + 1, instanceClone); 44 | // Position and resize new instance to frame 45 | instanceClone.x = frame.x; 46 | instanceClone.y = frame.y; 47 | instanceClone.resize(frame.width, frame.height); 48 | await overrideProperties(frame, instanceClone); 49 | frame.remove(); 50 | processedCount += 1; 51 | continue; 52 | } 53 | skippedCount += 1; 54 | continue; 55 | } 56 | 57 | return `${processedCount} processed, ${skippedCount} skipped`; 58 | } 59 | 60 | // Check if node is a component that can replace an instance 61 | function isEquivalentNode(frame: FrameNode, node: BaseNode): node is MasterOrInstance { 62 | if (node.type !== "INSTANCE" && node.type !== "COMPONENT") return false; 63 | if (node.name !== frame.name) return false; 64 | return true; 65 | } 66 | 67 | 68 | // Recursively overrides all properties on an instance 69 | async function overrideProperties(source: SceneNode, target: SceneNode) { 70 | 71 | 72 | // If source is text, the long and tedious process 73 | // of replacing all text properties begins! 74 | if (source.type === "TEXT" && target.type === "TEXT") { 75 | await overrideTextProperties(source, target); 76 | } 77 | 78 | // Try to override every other property 79 | // if it's present in source and not mixed 80 | allProperties.forEach(prop => { 81 | if (!(prop in source)) return; 82 | if (source[prop] === undefined) return; 83 | if (areEquivalent(target[prop], source[prop]) || isMixed(source[prop])) return; 84 | 85 | try { 86 | target[prop] = source[prop]; 87 | } catch (e) { console.error(e); } 88 | }); 89 | 90 | // Instances can be overriden too 91 | if (source.type === "INSTANCE" && target.type === "INSTANCE") { 92 | if (target.masterComponent.id !== source.masterComponent.id) { 93 | target.masterComponent = source.masterComponent; 94 | } 95 | } 96 | 97 | // Recursively change all children 98 | if (supportsChildren(source) && supportsChildren(target)) { 99 | for (let i = 0; i < target.children.length; i++) { 100 | const sourceChild = source.children[i] as SceneNode; 101 | const targetChild = target.children[i] as SceneNode; 102 | if (!sourceChild || !targetChild) continue; 103 | await overrideProperties(sourceChild, targetChild); 104 | } 105 | } 106 | 107 | } 108 | 109 | // Override all properties of text layers 110 | // Mixed properties are applied on each character because there are no alternatives 111 | async function overrideTextProperties(source: TextNode, target: TextNode) { 112 | 113 | // Collect mixed properties to replace them later 114 | const mixedProperties: string[] = []; 115 | const isStringSame = target.characters === source.characters; 116 | 117 | // Hacky methods to get character setter/getter methods 118 | // from property names 119 | const getPropertyAtChar = (prop: string, char: number) => { 120 | const rangeMethod = "getRange" + prop[0].toUpperCase() + prop.slice(1); 121 | return source[rangeMethod](char, char + 1); 122 | } 123 | 124 | const setPropertyAtChar = (prop: string, char: number, value: any) => { 125 | const rangeMethod = "setRange" + prop[0].toUpperCase() + prop.slice(1); 126 | target[rangeMethod](char, char + 1, value); 127 | } 128 | 129 | // Start off by checking if characters are the same and changing them 130 | const fontName = source.getRangeFontName(0, 1) as FontName; 131 | 132 | await figma.loadFontAsync(fontName).then(() => { 133 | // With font loaded we reset all properties to 134 | // ones applied to the first character of source 135 | textProperties.forEach(prop => { 136 | if (isMixed(source[prop])) { 137 | mixedProperties.push(prop); 138 | } 139 | if (isStringSame) return; 140 | const value = getPropertyAtChar(prop, 0); 141 | if (areEquivalent(target[prop], value) 142 | && value !== undefined) return; 143 | target[prop] = value; 144 | }); 145 | 146 | if (!isStringSame) { 147 | target.characters = source.characters; 148 | } 149 | }).catch((e) => { console.error(e); }); 150 | 151 | // All properties are set on the whole text without mixed ones! 152 | if (mixedProperties.length === 0) { 153 | return; 154 | } 155 | 156 | // Don't override too long texts with many different properties 157 | // because it would take too much time 158 | if (target.characters.length > 150 159 | && mixedProperties.length > 2) { 160 | return; 161 | } 162 | 163 | // Load all fonts on the text block 164 | for (let char = 0; char < target.characters.length; char++) { 165 | const fontName = source.getRangeFontName(char, char + 1) as FontName 166 | await figma.loadFontAsync(fontName).catch((e) => { console.error(e); }); 167 | } 168 | 169 | // Set properties character by character on mixed fields 170 | // This can't be done in the previous loop 171 | for (let char = 0; char < target.characters.length; char++) { 172 | for (let i = 0; i < mixedProperties.length; i++) { 173 | const prop = mixedProperties[i]; 174 | try { 175 | const value = getPropertyAtChar(prop, char); 176 | if (value === null) continue; 177 | setPropertyAtChar(prop, char, value); 178 | } catch (e) { console.error(e); } 179 | } 180 | } 181 | } 182 | 183 | // Returns type predicate and true if node supports children 184 | function supportsChildren(node: SceneNode): 185 | node is FrameNode | ComponentNode | InstanceNode | BooleanOperationNode { 186 | return node.type === "FRAME" || node.type === "COMPONENT" || 187 | node.type === "INSTANCE" || node.type === "BOOLEAN_OPERATION"; 188 | } 189 | 190 | function isMixed(property: any) { 191 | return property === figma.mixed && typeof property === "symbol"; 192 | } 193 | 194 | // Checks if two objects are equivalent 195 | function areEquivalent(a: any, b: any) { 196 | if (a === b) { 197 | return true; 198 | } 199 | if (!(a instanceof Object || b instanceof Object)) { 200 | return false; 201 | } 202 | 203 | if (toType(a) !== toType(b)) { 204 | return false; 205 | } 206 | 207 | if (Array.isArray(a) && Array.isArray(b)) { 208 | if (a.length !== b.length) { 209 | return false; 210 | } 211 | if (a.length === b.length) { 212 | for (let i = 0; i < a.length; i++) { 213 | if (!areEquivalent(a[i], b[i])) { 214 | return false; 215 | } 216 | } 217 | } 218 | return true; 219 | } 220 | 221 | // Create arrays of property names 222 | let aProps = Object.getOwnPropertyNames(a); 223 | let bProps = Object.getOwnPropertyNames(b); 224 | 225 | // If number of properties is different, 226 | // objects are not equivalent 227 | if (aProps.length !== bProps.length) { 228 | return false; 229 | } 230 | for (let i = 0; i < aProps.length; i++) { 231 | let prop = aProps[i]; 232 | // Recursion: if values of the same property 233 | // are not equal, objects are not equivalent 234 | if (!areEquivalent(a[prop], b[prop])) { 235 | return false; 236 | } 237 | } 238 | // If we made it this far, objects 239 | // are considered equivalent 240 | return true; 241 | } 242 | 243 | // Returns type of an object 244 | function toType(obj: any) { 245 | return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); 246 | } 247 | 248 | // A list of all overrrideable properties in Figma 249 | const allProperties = [ 250 | "visible", 251 | "locked", 252 | "opacity", 253 | "blendMode", 254 | 255 | "effects", 256 | "effectStyleId", 257 | // frame 258 | "backgrounds", 259 | "layoutGrids", 260 | "clipsContent", 261 | "guides", // array 262 | "gridStyleId", 263 | "backgroundStyleId", 264 | // geometry 265 | "fills", // array 266 | "strokes", // array 267 | "strokes", 268 | "strokeAlign", 269 | "strokeCap", 270 | "strokeJoin", 271 | "dashPattern", 272 | "fillStyleId", 273 | "strokeStyleId", 274 | 275 | "cornerRadius", 276 | "cornerSmoothing", 277 | 278 | "exportSettings", // array 279 | // component instance 280 | "masterComponent", // check its id 281 | // text 282 | "autoRename", 283 | "textAlignHorizontal", 284 | "textAlignVertical", 285 | "paragraphIndent", 286 | "paragraphSpacing", 287 | ]; 288 | 289 | // Text properties that can be overriden on individual characters 290 | const textProperties = [ 291 | "fills", 292 | "fillStyleId", 293 | 294 | "fontSize", 295 | "fontName", 296 | "textCase", 297 | "textDecoration", 298 | "letterSpacing", 299 | "lineHeight", 300 | "textStyleId", 301 | ]; 302 | 303 | // Methods are async, close plugin when they are resolved 304 | main().then(msg => { 305 | figma.closePlugin(msg); 306 | }); 307 | -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Global variable with Figma's plugin API. 2 | declare const figma: PluginAPI 3 | declare const __html__: string 4 | 5 | interface PluginAPI { 6 | readonly apiVersion: "1.0.0" 7 | readonly command: string 8 | readonly root: DocumentNode 9 | readonly viewport: ViewportAPI 10 | closePlugin(message?: string): void 11 | 12 | showUI(html: string, options?: ShowUIOptions): void 13 | readonly ui: UIAPI 14 | 15 | readonly clientStorage: ClientStorageAPI 16 | 17 | getNodeById(id: string): BaseNode | null 18 | getStyleById(id: string): BaseStyle | null 19 | 20 | currentPage: PageNode 21 | 22 | readonly mixed: symbol 23 | 24 | createRectangle(): RectangleNode 25 | createLine(): LineNode 26 | createEllipse(): EllipseNode 27 | createPolygon(): PolygonNode 28 | createStar(): StarNode 29 | createVector(): VectorNode 30 | createText(): TextNode 31 | createBooleanOperation(): BooleanOperationNode 32 | createFrame(): FrameNode 33 | createComponent(): ComponentNode 34 | createPage(): PageNode 35 | createSlice(): SliceNode 36 | 37 | createPaintStyle(): PaintStyle 38 | createTextStyle(): TextStyle 39 | createEffectStyle(): EffectStyle 40 | createGridStyle(): GridStyle 41 | 42 | importComponentByKeyAsync(key: string): Promise 43 | importStyleByKeyAsync(key: string): Promise 44 | 45 | listAvailableFontsAsync(): Promise 46 | loadFontAsync(fontName: FontName): Promise 47 | readonly hasMissingFont: boolean 48 | 49 | createNodeFromSvg(svg: string): FrameNode 50 | 51 | createImage(data: Uint8Array): Image 52 | getImageByHash(hash: string): Image 53 | 54 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode 55 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 56 | } 57 | 58 | interface ClientStorageAPI { 59 | getAsync(key: string): Promise 60 | setAsync(key: string, value: any): Promise 61 | } 62 | 63 | type ShowUIOptions = { 64 | visible?: boolean, 65 | width?: number, 66 | height?: number, 67 | } 68 | 69 | type UIPostMessageOptions = { 70 | targetOrigin?: string, 71 | } 72 | 73 | type OnMessageProperties = { 74 | sourceOrigin: string, 75 | } 76 | 77 | interface UIAPI { 78 | show(): void 79 | hide(): void 80 | resize(width: number, height: number): void 81 | close(): void 82 | 83 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 84 | onmessage: ((pluginMessage: any, props: OnMessageProperties) => void) | undefined 85 | } 86 | 87 | interface ViewportAPI { 88 | center: { x: number, y: number } 89 | zoom: number 90 | scrollAndZoomIntoView(nodes: ReadonlyArray) 91 | } 92 | 93 | //////////////////////////////////////////////////////////////////////////////// 94 | // Datatypes 95 | 96 | type Transform = [ 97 | [number, number, number], 98 | [number, number, number] 99 | ] 100 | 101 | interface Vector { 102 | readonly x: number 103 | readonly y: number 104 | } 105 | 106 | interface RGB { 107 | readonly r: number 108 | readonly g: number 109 | readonly b: number 110 | } 111 | 112 | interface RGBA { 113 | readonly r: number 114 | readonly g: number 115 | readonly b: number 116 | readonly a: number 117 | } 118 | 119 | interface FontName { 120 | readonly family: string 121 | readonly style: string 122 | } 123 | 124 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 125 | 126 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 127 | 128 | interface ArcData { 129 | readonly startingAngle: number 130 | readonly endingAngle: number 131 | readonly innerRadius: number 132 | } 133 | 134 | interface ShadowEffect { 135 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 136 | readonly color: RGBA 137 | readonly offset: Vector 138 | readonly radius: number 139 | readonly visible: boolean 140 | readonly blendMode: BlendMode 141 | } 142 | 143 | interface BlurEffect { 144 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 145 | readonly radius: number 146 | readonly visible: boolean 147 | } 148 | 149 | type Effect = ShadowEffect | BlurEffect 150 | 151 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 152 | 153 | interface Constraints { 154 | readonly horizontal: ConstraintType 155 | readonly vertical: ConstraintType 156 | } 157 | 158 | interface ColorStop { 159 | readonly position: number 160 | readonly color: RGBA 161 | } 162 | 163 | interface ImageFilters { 164 | exposure?: number 165 | contrast?: number 166 | saturation?: number 167 | temperature?: number 168 | tint?: number 169 | highlights?: number 170 | shadows?: number 171 | } 172 | 173 | interface SolidPaint { 174 | readonly type: "SOLID" 175 | readonly color: RGB 176 | 177 | readonly visible?: boolean 178 | readonly opacity?: number 179 | readonly blendMode?: BlendMode 180 | } 181 | 182 | interface GradientPaint { 183 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 184 | readonly gradientTransform: Transform 185 | readonly gradientStops: ReadonlyArray 186 | 187 | readonly visible?: boolean 188 | readonly opacity?: number 189 | readonly blendMode?: BlendMode 190 | } 191 | 192 | interface ImagePaint { 193 | readonly type: "IMAGE" 194 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 195 | readonly imageHash: string | null 196 | readonly imageTransform?: Transform // setting for "CROP" 197 | readonly scalingFactor?: number // setting for "TILE" 198 | readonly filters?: ImageFilters 199 | 200 | readonly visible?: boolean 201 | readonly opacity?: number 202 | readonly blendMode?: BlendMode 203 | } 204 | 205 | type Paint = SolidPaint | GradientPaint | ImagePaint 206 | 207 | interface Guide { 208 | readonly axis: "X" | "Y" 209 | readonly offset: number 210 | } 211 | 212 | interface RowsColsLayoutGrid { 213 | readonly pattern: "ROWS" | "COLUMNS" 214 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 215 | readonly gutterSize: number 216 | 217 | readonly count: number // Infinity when "Auto" is set in the UI 218 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 219 | readonly offset?: number // Not set for alignment: "CENTER" 220 | 221 | readonly visible?: boolean 222 | readonly color?: RGBA 223 | } 224 | 225 | interface GridLayoutGrid { 226 | readonly pattern: "GRID" 227 | readonly sectionSize: number 228 | 229 | readonly visible?: boolean 230 | readonly color?: RGBA 231 | } 232 | 233 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 234 | 235 | interface ExportSettingsConstraints { 236 | type: "SCALE" | "WIDTH" | "HEIGHT" 237 | value: number 238 | } 239 | 240 | interface ExportSettingsImage { 241 | format: "JPG" | "PNG" 242 | contentsOnly?: boolean // defaults to true 243 | suffix?: string 244 | constraint?: ExportSettingsConstraints 245 | } 246 | 247 | interface ExportSettingsSVG { 248 | format: "SVG" 249 | contentsOnly?: boolean // defaults to true 250 | suffix?: string 251 | svgOutlineText?: boolean // defaults to true 252 | svgIdAttribute?: boolean // defaults to false 253 | svgSimplifyStroke?: boolean // defaults to true 254 | } 255 | 256 | interface ExportSettingsPDF { 257 | format: "PDF" 258 | contentsOnly?: boolean // defaults to true 259 | suffix?: string 260 | } 261 | 262 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 263 | 264 | type WindingRule = "NONZERO" | "EVENODD" 265 | 266 | interface VectorVertex { 267 | readonly x: number 268 | readonly y: number 269 | readonly strokeCap?: StrokeCap 270 | readonly strokeJoin?: StrokeJoin 271 | readonly cornerRadius?: number 272 | readonly handleMirroring?: HandleMirroring 273 | } 274 | 275 | interface VectorSegment { 276 | readonly start: number 277 | readonly end: number 278 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 279 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 280 | } 281 | 282 | interface VectorRegion { 283 | readonly windingRule: WindingRule 284 | readonly loops: ReadonlyArray> 285 | } 286 | 287 | interface VectorNetwork { 288 | readonly vertices: ReadonlyArray 289 | readonly segments: ReadonlyArray 290 | readonly regions?: ReadonlyArray // Defaults to [] 291 | } 292 | 293 | interface VectorPath { 294 | readonly windingRule: WindingRule | "NONE" 295 | readonly data: string 296 | } 297 | 298 | type VectorPaths = ReadonlyArray 299 | 300 | type LetterSpacing = { 301 | readonly value: number 302 | readonly unit: "PIXELS" | "PERCENT" 303 | } 304 | 305 | type LineHeight = { 306 | readonly value: number 307 | readonly unit: "PIXELS" | "PERCENT" 308 | } | { 309 | readonly unit: "AUTO" 310 | } 311 | 312 | type BlendMode = 313 | "PASS_THROUGH" | 314 | "NORMAL" | 315 | "DARKEN" | 316 | "MULTIPLY" | 317 | "LINEAR_BURN" | 318 | "COLOR_BURN" | 319 | "LIGHTEN" | 320 | "SCREEN" | 321 | "LINEAR_DODGE" | 322 | "COLOR_DODGE" | 323 | "OVERLAY" | 324 | "SOFT_LIGHT" | 325 | "HARD_LIGHT" | 326 | "DIFFERENCE" | 327 | "EXCLUSION" | 328 | "HUE" | 329 | "SATURATION" | 330 | "COLOR" | 331 | "LUMINOSITY" 332 | 333 | interface Font { 334 | fontName: FontName 335 | } 336 | 337 | //////////////////////////////////////////////////////////////////////////////// 338 | // Mixins 339 | 340 | interface BaseNodeMixin { 341 | readonly id: string 342 | readonly parent: (BaseNode & ChildrenMixin) | null 343 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 344 | readonly removed: boolean 345 | toString(): string 346 | remove(): void 347 | 348 | getPluginData(key: string): string 349 | setPluginData(key: string, value: string): void 350 | 351 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 352 | // be a name related to your plugin. Other plugins will be able to read this data. 353 | getSharedPluginData(namespace: string, key: string): string 354 | setSharedPluginData(namespace: string, key: string, value: string): void 355 | } 356 | 357 | interface SceneNodeMixin { 358 | visible: boolean 359 | locked: boolean 360 | } 361 | 362 | interface ChildrenMixin { 363 | readonly children: ReadonlyArray 364 | 365 | appendChild(child: BaseNode): void 366 | insertChild(index: number, child: BaseNode): void 367 | 368 | findAll(callback?: (node: BaseNode) => boolean): ReadonlyArray 369 | findOne(callback: (node: BaseNode) => boolean): BaseNode | null 370 | } 371 | 372 | interface ConstraintMixin { 373 | constraints: Constraints 374 | } 375 | 376 | interface LayoutMixin { 377 | readonly absoluteTransform: Transform 378 | relativeTransform: Transform 379 | x: number 380 | y: number 381 | rotation: number // In degrees 382 | 383 | readonly width: number 384 | readonly height: number 385 | 386 | resize(width: number, height: number): void 387 | resizeWithoutConstraints(width: number, height: number): void 388 | } 389 | 390 | interface BlendMixin { 391 | opacity: number 392 | blendMode: BlendMode 393 | isMask: boolean 394 | effects: ReadonlyArray 395 | effectStyleId: string 396 | } 397 | 398 | interface FrameMixin { 399 | backgrounds: ReadonlyArray 400 | layoutGrids: ReadonlyArray 401 | clipsContent: boolean 402 | guides: ReadonlyArray 403 | gridStyleId: string 404 | backgroundStyleId: string 405 | } 406 | 407 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 408 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 409 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 410 | 411 | interface GeometryMixin { 412 | fills: ReadonlyArray | symbol 413 | strokes: ReadonlyArray 414 | strokeWeight: number 415 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 416 | strokeCap: StrokeCap | symbol 417 | strokeJoin: StrokeJoin | symbol 418 | dashPattern: ReadonlyArray 419 | fillStyleId: string | symbol 420 | strokeStyleId: string 421 | } 422 | 423 | interface CornerMixin { 424 | cornerRadius: number | symbol 425 | cornerSmoothing: number 426 | } 427 | 428 | interface ExportMixin { 429 | exportSettings: ExportSettings[] 430 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 431 | } 432 | 433 | interface DefaultShapeMixin extends 434 | BaseNodeMixin, SceneNodeMixin, 435 | BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { 436 | } 437 | 438 | interface DefaultContainerMixin extends 439 | BaseNodeMixin, SceneNodeMixin, 440 | ChildrenMixin, FrameMixin, 441 | BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { 442 | } 443 | 444 | //////////////////////////////////////////////////////////////////////////////// 445 | // Nodes 446 | 447 | interface DocumentNode extends BaseNodeMixin, ChildrenMixin { 448 | readonly type: "DOCUMENT" 449 | } 450 | 451 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 452 | readonly type: "PAGE" 453 | clone(): PageNode 454 | 455 | guides: ReadonlyArray 456 | selection: ReadonlyArray 457 | } 458 | 459 | interface FrameNode extends DefaultContainerMixin { 460 | readonly type: "FRAME" | "GROUP" 461 | clone(): FrameNode 462 | } 463 | 464 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 465 | readonly type: "SLICE" 466 | clone(): SliceNode 467 | } 468 | 469 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 470 | readonly type: "RECTANGLE" 471 | clone(): RectangleNode 472 | topLeftRadius: number 473 | topRightRadius: number 474 | bottomLeftRadius: number 475 | bottomRightRadius: number 476 | } 477 | 478 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 479 | readonly type: "LINE" 480 | clone(): LineNode 481 | } 482 | 483 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 484 | readonly type: "ELLIPSE" 485 | clone(): EllipseNode 486 | arcData: ArcData 487 | } 488 | 489 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 490 | readonly type: "POLYGON" 491 | clone(): PolygonNode 492 | pointCount: number 493 | } 494 | 495 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 496 | readonly type: "STAR" 497 | clone(): StarNode 498 | pointCount: number 499 | innerRadius: number 500 | } 501 | 502 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 503 | readonly type: "VECTOR" 504 | clone(): VectorNode 505 | vectorNetwork: VectorNetwork 506 | vectorPaths: VectorPaths 507 | handleMirroring: HandleMirroring | symbol 508 | } 509 | 510 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 511 | readonly type: "TEXT" 512 | clone(): TextNode 513 | characters: string 514 | readonly hasMissingFont: boolean 515 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 516 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 517 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 518 | paragraphIndent: number 519 | paragraphSpacing: number 520 | autoRename: boolean 521 | 522 | textStyleId: string | symbol 523 | fontSize: number | symbol 524 | fontName: FontName | symbol 525 | textCase: TextCase | symbol 526 | textDecoration: TextDecoration | symbol 527 | letterSpacing: LetterSpacing | symbol 528 | lineHeight: LineHeight | symbol 529 | 530 | getRangeFontSize(start: number, end: number): number | symbol 531 | setRangeFontSize(start: number, end: number, value: number): void 532 | getRangeFontName(start: number, end: number): FontName | symbol 533 | setRangeFontName(start: number, end: number, value: FontName): void 534 | getRangeTextCase(start: number, end: number): TextCase | symbol 535 | setRangeTextCase(start: number, end: number, value: TextCase): void 536 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol 537 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 538 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol 539 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 540 | getRangeLineHeight(start: number, end: number): LineHeight | symbol 541 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 542 | getRangeFills(start: number, end: number): Paint[] | symbol 543 | setRangeFills(start: number, end: number, value: Paint[]): void 544 | getRangeTextStyleId(start: number, end: number): string | symbol 545 | setRangeTextStyleId(start: number, end: number, value: string): void 546 | getRangeFillStyleId(start: number, end: number): string | symbol 547 | setRangeFillStyleId(start: number, end: number, value: string): void 548 | } 549 | 550 | interface ComponentNode extends DefaultContainerMixin { 551 | readonly type: "COMPONENT" 552 | clone(): ComponentNode 553 | 554 | createInstance(): InstanceNode 555 | description: string 556 | readonly remote: boolean 557 | readonly key: string // The key to use with "importComponentByKeyAsync" 558 | } 559 | 560 | interface InstanceNode extends DefaultContainerMixin { 561 | readonly type: "INSTANCE" 562 | clone(): InstanceNode 563 | masterComponent: ComponentNode 564 | } 565 | 566 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 567 | readonly type: "BOOLEAN_OPERATION" 568 | clone(): BooleanOperationNode 569 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 570 | } 571 | 572 | type BaseNode = 573 | DocumentNode | 574 | PageNode | 575 | SceneNode 576 | 577 | type SceneNode = 578 | SliceNode | 579 | FrameNode | 580 | ComponentNode | 581 | InstanceNode | 582 | BooleanOperationNode | 583 | VectorNode | 584 | StarNode | 585 | LineNode | 586 | EllipseNode | 587 | PolygonNode | 588 | RectangleNode | 589 | TextNode 590 | 591 | type NodeType = 592 | "DOCUMENT" | 593 | "PAGE" | 594 | "SLICE" | 595 | "FRAME" | 596 | "GROUP" | 597 | "COMPONENT" | 598 | "INSTANCE" | 599 | "BOOLEAN_OPERATION" | 600 | "VECTOR" | 601 | "STAR" | 602 | "LINE" | 603 | "ELLIPSE" | 604 | "POLYGON" | 605 | "RECTANGLE" | 606 | "TEXT" 607 | 608 | //////////////////////////////////////////////////////////////////////////////// 609 | // Styles 610 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 611 | 612 | interface BaseStyle { 613 | readonly id: string 614 | readonly type: StyleType 615 | name: string 616 | description: string 617 | remote: boolean 618 | readonly key: string // The key to use with "importStyleByKeyAsync" 619 | remove(): void 620 | } 621 | 622 | interface PaintStyle extends BaseStyle { 623 | type: "PAINT" 624 | paints: ReadonlyArray 625 | } 626 | 627 | interface TextStyle extends BaseStyle { 628 | type: "TEXT" 629 | fontSize: number 630 | textDecoration: TextDecoration 631 | fontName: FontName 632 | letterSpacing: LetterSpacing 633 | lineHeight: LineHeight 634 | paragraphIndent: number 635 | paragraphSpacing: number 636 | textCase: TextCase 637 | } 638 | 639 | interface EffectStyle extends BaseStyle { 640 | type: "EFFECT" 641 | effects: ReadonlyArray 642 | } 643 | 644 | interface GridStyle extends BaseStyle { 645 | type: "GRID" 646 | layoutGrids: ReadonlyArray 647 | } 648 | 649 | //////////////////////////////////////////////////////////////////////////////// 650 | // Other 651 | 652 | interface Image { 653 | readonly hash: string 654 | getBytesAsync(): Promise 655 | } 656 | -------------------------------------------------------------------------------- /icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renancamm/figma-reattache-instance/3b4b1a8402397e788ecd87a9c0806e78c65c038b/icon_128.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reattach Instance", 3 | "id": "741415678427267506", 4 | "api": "1.0.0", 5 | "main": "code.js" 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6" 4 | } 5 | } 6 | --------------------------------------------------------------------------------