├── .gitignore
├── src
├── ui.html
├── cover_art_1920x960.png
├── icon_round_128x128.png
├── icon_square_128x128.png
├── maximum_override_logo_2x.png
├── logo.svg
├── icon.svg
├── ui.scss
├── util.ts
├── code.ts
├── ui.tsx
└── figma-plugin-ds.css
├── manifest.json
├── tsconfig.json
├── .vscode
└── launch.json
├── package.json
├── dist
├── code.css
└── ui.js.map
├── webpack.config.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/src/ui.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/cover_art_1920x960.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrookedGrin/maximum-override/HEAD/src/cover_art_1920x960.png
--------------------------------------------------------------------------------
/src/icon_round_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrookedGrin/maximum-override/HEAD/src/icon_round_128x128.png
--------------------------------------------------------------------------------
/src/icon_square_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrookedGrin/maximum-override/HEAD/src/icon_square_128x128.png
--------------------------------------------------------------------------------
/src/maximum_override_logo_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CrookedGrin/maximum-override/HEAD/src/maximum_override_logo_2x.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Maximum Override",
3 | "id": "844697202715835091",
4 | "api": "1.0.0",
5 | "main": "dist/code.js",
6 | "ui": "dist/ui.html",
7 | "editorType": ["figma"]
8 | }
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "jsx": "react",
5 | "moduleResolution": "node",
6 | "typeRoots": [
7 | "./node_modules/@types",
8 | "./node_modules/@figma"
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:8080",
12 | "webRoot": "${workspaceFolder}"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/src/icon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "maximum-override",
3 | "version": "1.1.1",
4 | "description": "Figma plugin for comparing and swapping props",
5 | "main": "dist/code.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "npx webpack --mode=development --watch"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "deep-equal": "^2.0.3",
14 | "figma-plugin-ds": "^0.1.8",
15 | "html-webpack-inline-source-plugin": "0.0.10",
16 | "html-webpack-plugin": "^3.2.0",
17 | "react": "^16.13.1",
18 | "react-dom": "^16.13.1",
19 | "ts-loader": "^6.0.4",
20 | "typescript": "^3.5.3",
21 | "url-loader": "^2.1.0",
22 | "webpack": "^4.43.0",
23 | "webpack-cli": "^3.3.6"
24 | },
25 | "devDependencies": {
26 | "@figma/plugin-typings": "^1.28.0",
27 | "@types/node-sass": "^4.11.1",
28 | "@types/react": "^16.9.35",
29 | "css-loader": "^3.5.3",
30 | "mini-css-extract-plugin": "^0.9.0",
31 | "node-sass": "^7.0.3",
32 | "sass-loader": "^8.0.2",
33 | "style-loader": "^0.23.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/dist/code.css:
--------------------------------------------------------------------------------
1 | body {
2 | font: 12px sans-serif;
3 | text-align: center;
4 | margin: 20px; }
5 |
6 | .buttons {
7 | display: flex;
8 | flex-flow: column nowrap; }
9 | .buttons button {
10 | border-radius: 5px;
11 | background: white;
12 | color: black;
13 | border: none;
14 | padding: 8px 15px;
15 | margin: 2px;
16 | box-shadow: inset 0 0 0 1px black;
17 | outline: none; }
18 |
19 | .diff {
20 | margin: 16px;
21 | display: flex;
22 | flex-flow: row nowrap; }
23 |
24 | .overrides {
25 | text-align: left; }
26 | .overrides .props {
27 | display: flex;
28 | flex-flow: column nowrap; }
29 | .overrides .props .prop {
30 | display: block; }
31 | .overrides .props .rgbColor {
32 | display: block;
33 | width: 16px;
34 | height: 16px; }
35 |
36 |
37 | /*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvdWkuc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQUNBO0FBQ0E7QUFDQSxlQUFlOztBQUVmO0FBQ0E7QUFDQSwyQkFBMkI7QUFDM0I7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGtCQUFrQjs7QUFFbEI7QUFDQTtBQUNBO0FBQ0Esd0JBQXdCOztBQUV4QjtBQUNBLG1CQUFtQjtBQUNuQjtBQUNBO0FBQ0EsNkJBQTZCO0FBQzdCO0FBQ0EscUJBQXFCO0FBQ3JCO0FBQ0E7QUFDQTtBQUNBLG1CQUFtQiIsImZpbGUiOiJjb2RlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbImJvZHkge1xuICBmb250OiAxMnB4IHNhbnMtc2VyaWY7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbiAgbWFyZ2luOiAyMHB4OyB9XG5cbi5idXR0b25zIHtcbiAgZGlzcGxheTogZmxleDtcbiAgZmxleC1mbG93OiBjb2x1bW4gbm93cmFwOyB9XG4gIC5idXR0b25zIGJ1dHRvbiB7XG4gICAgYm9yZGVyLXJhZGl1czogNXB4O1xuICAgIGJhY2tncm91bmQ6IHdoaXRlO1xuICAgIGNvbG9yOiBibGFjaztcbiAgICBib3JkZXI6IG5vbmU7XG4gICAgcGFkZGluZzogOHB4IDE1cHg7XG4gICAgbWFyZ2luOiAycHg7XG4gICAgYm94LXNoYWRvdzogaW5zZXQgMCAwIDAgMXB4IGJsYWNrO1xuICAgIG91dGxpbmU6IG5vbmU7IH1cblxuLmRpZmYge1xuICBtYXJnaW46IDE2cHg7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIGZsZXgtZmxvdzogcm93IG5vd3JhcDsgfVxuXG4ub3ZlcnJpZGVzIHtcbiAgdGV4dC1hbGlnbjogbGVmdDsgfVxuICAub3ZlcnJpZGVzIC5wcm9wcyB7XG4gICAgZGlzcGxheTogZmxleDtcbiAgICBmbGV4LWZsb3c6IGNvbHVtbiBub3dyYXA7IH1cbiAgICAub3ZlcnJpZGVzIC5wcm9wcyAucHJvcCB7XG4gICAgICBkaXNwbGF5OiBibG9jazsgfVxuICAgIC5vdmVycmlkZXMgLnByb3BzIC5yZ2JDb2xvciB7XG4gICAgICBkaXNwbGF5OiBibG9jaztcbiAgICAgIHdpZHRoOiAxNnB4O1xuICAgICAgaGVpZ2h0OiAxNnB4OyB9XG4iXSwic291cmNlUm9vdCI6IiJ9*/
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
4 | const path = require('path')
5 | const isDevelopment = process.env.NODE_ENV === 'development'
6 | const webpack = require('webpack')
7 |
8 | module.exports = (env, argv) => ({
9 | mode: argv.mode === 'production' ? 'production' : 'development',
10 |
11 | // This is necessary because Figma's 'eval' works differently than normal eval
12 | devtool: argv.mode === 'production' ? false : 'inline-source-map',
13 |
14 | entry: {
15 | ui: './src/ui.tsx', // The entry point for your UI code
16 | code: './src/code.ts', // The entry point for your plugin code
17 | },
18 |
19 | module: {
20 | rules: [
21 | // Converts TypeScript code to JavaScript
22 | { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
23 |
24 | // Enables including CSS by doing "import './file.css'" in your TypeScript code
25 | { test: /\.css$/, loader: [{ loader: 'style-loader' }, { loader: 'css-loader' }] },
26 |
27 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI
28 | { test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: [{ loader: 'url-loader' }] },
29 |
30 | {
31 | test: /\.s(a|c)ss$/,
32 | loader: [
33 | isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
34 | 'css-loader',
35 | {
36 | loader: 'sass-loader',
37 | options: {
38 | sourceMap: isDevelopment
39 | }
40 | }
41 | ]
42 | }
43 |
44 | ],
45 | },
46 |
47 | // Webpack tries these extensions for you if you omit the extension like "import './file'"
48 | resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.scss'] },
49 |
50 | output: {
51 | filename: '[name].js',
52 | path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist"
53 | },
54 |
55 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it
56 | plugins: [
57 | new webpack.DefinePlugin({
58 | 'global': {} // Fix missing symbol error when running in developer VM
59 | }),
60 | new HtmlWebpackPlugin({
61 | template: './src/ui.html',
62 | filename: 'ui.html',
63 | inlineSource: '.(js)$',
64 | chunks: ['ui'],
65 | }),
66 | new HtmlWebpackInlineSourcePlugin(),
67 | new MiniCssExtractPlugin({
68 | filename: '[name].css',
69 | chunkFilename: '[id].css'
70 | })
71 | ],
72 | })
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Maximium Override
2 |
3 |
4 |
5 | ### Take control of your Figma overrides!
6 |
7 | Compare objects to see what's different, then copy and paste the changes. Compare instances to their default main component and copy and paste the overrides.
8 |
9 | [https://www.figma.com/community/plugin/844697202715835091/Maximum-Override](https://www.figma.com/community/plugin/844697202715835091/Maximum-Override)
10 |
11 |
12 |
13 | ---
14 |
15 | ## Features
16 |
17 | ### **1. Copy and paste overrides between symbols**
18 |
19 | Two of Figma's greatest strengths are:
20 | 1. Overriding colors, text, and other properties on component instances, and
21 | 2. Swapping out instances of components, such as a icons or states.
22 |
23 | But when you swap an instance, you lose all the overrides you've made, including changes to nested instances within a parent component.
24 |
25 | Maximum Override allows you to copy your overrides, swap out an instance, and then paste the overrides back onto the new instance, to retain all of your changes. This includes nested instances that you've swapped with a different symbol.
26 |
27 |
28 | ### **2. Compare properties between objects**
29 |
30 | Maximum Override also allows you to select any two nodes on the stage and compare them against each other to see what's different. (In software, this is known as a "diff".) You can then copy the changes from object A to object B, or vice-versa. If you select a single instance, Maximum Override will compare it against its own main component.
31 |
32 |
33 |
34 | ---
35 | ## Usage
36 |
37 |
38 |
39 | Install and run Maximum Override like any other Figma plugin. See [Figma's plugin guide](https://help.figma.com/hc/en-us/articles/360040450413-Browse-and-Install-Plugins#Install_a_Plugin).
40 |
41 | ### Comparing
42 |
43 | Select a single component instance to compare it against its own main component.
44 |
45 | Select two items to compare them to each other. These can be any type of object on stage, including instances. Click "Swap" to change which is the source and which is the target of the overrides.
46 |
47 | By default, Maximum Override will collapse all the nodes that don't contain overrides, but you can click the title of any node to expand and see its children.
48 |
49 | To save space while you work, you can also collapse the header, and the interface window will collapse as well.
50 |
51 | Within overrides, hover over colors to see the corresponding hex value.
52 |
53 | ### Copying
54 |
55 | Click "Copy overrides" to save the current set of target overrides. This data is saved locally on your computer and will persist when you close the plugin or close Figma.
56 |
57 | ### Pasting
58 |
59 | Select a single item and click "Paste overrides" to apply the saved overrides to the selected item. If the structure of the target item doesn't match the structure that the overrides were originally pulled from, Maximum Override will still attempt to apply the overrides, but results may vary.
60 |
61 |
62 |
63 | ---
64 |
65 | ### Not Supported (yet) / Known Issues
66 |
67 | #### Very large components
68 |
69 | Components with large numbers of layers and nested instances may be quite slow to process. Future versions may include a progress bar and other optimizations.
70 |
71 | #### Multiple text styles
72 |
73 | Text layers get very complicated, since each character can have its own set of styles and overrides. Maximum Override currently assumes that there is only one font / style applied to a given text field.
74 |
75 |
76 |
77 | ---
78 |
79 | ### Development
80 |
81 |
82 | To build:
83 |
84 | $ npm install
85 |
86 | To run locally:
87 |
88 | $ npm run start
89 |
--------------------------------------------------------------------------------
/src/ui.scss:
--------------------------------------------------------------------------------
1 | .MaximumOverride {
2 | padding: 12px;
3 | --mo-blue-light: #56CCF2;
4 | --mo-blue-med: #6DBFFA;
5 | }
6 |
7 | ::-webkit-scrollbar {
8 | width: 40px;
9 | }
10 |
11 | .emptyState {
12 | position: absolute;
13 | display: flex;
14 | flex-flow: column nowrap;
15 | justify-content: center;
16 | align-items: center;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | right: 0;
21 | background: var(--silver);
22 | text-align: left;
23 | z-index: 100;
24 |
25 | p {
26 | width: 320px;
27 | margin-bottom: 0.25em;
28 | font-size: var(--font-size-xlarge);
29 |
30 | &.centered {
31 | text-align: center;
32 | }
33 | }
34 |
35 | .logo {
36 | width: 320px;
37 | height: 200px;
38 | background: url(./maximum_override_logo_2x.png);
39 | background-position: 50% 50%;
40 | background-size: contain;
41 | background-repeat: no-repeat;
42 | }
43 | }
44 |
45 | .expand-collapse {
46 | border-radius: 100%;
47 | opacity: 0.5;
48 | transition: opacity 0.1s;
49 |
50 | .icon {
51 | background-size: 150%;
52 | }
53 | }
54 |
55 | .buttons {
56 | display: flex;
57 | flex-flow: row nowrap;
58 | margin-bottom: 8px;
59 |
60 | button {
61 | margin-right: 8px;
62 | cursor: pointer;
63 |
64 | &:disabled {
65 | cursor: default;
66 | }
67 | }
68 |
69 | }
70 |
71 | .icon {
72 | background-size: contain;
73 | }
74 |
75 | .content {
76 | background-color: var(--silver);
77 | border-radius: 8px;
78 | overflow: hidden;
79 | }
80 |
81 | .header {
82 | display: flex;
83 | flex-flow: row nowrap;
84 | align-items: center;
85 | font-weight: bold;
86 | font-size: var(--font-size-large);
87 | // border-bottom: 1px solid rgba(0,0,0,0.15);
88 | background-color: rgba(0,0,0,0.1);
89 | padding: 0 14px 0 0;
90 | height: 36px;
91 | overflow: hidden;
92 | cursor: pointer;
93 |
94 | .expand-collapse {
95 | margin-left: -2px;
96 | margin-right: -8px;
97 | }
98 |
99 | .icon {
100 | width: 24px;
101 | height: 24px;
102 | background-position: center;
103 | pointer-events: none;
104 | }
105 |
106 | .arrow {
107 | margin: 0 4px;
108 | }
109 |
110 | .compare-node {
111 | display: block;
112 | overflow: hidden;
113 | white-space: nowrap;
114 | text-overflow: ellipsis;
115 | max-width: 40%;
116 | }
117 |
118 | &:hover {
119 | .expand-collapse {
120 | opacity: 1;
121 | }
122 | }
123 |
124 | &--loading {
125 | padding-left: 22px;
126 | }
127 |
128 | }
129 |
130 | .diff {
131 | position: relative;
132 | font-size: var(--font-size-small);
133 | display: flex;
134 | flex-flow: row nowrap;
135 | background-color: var(--silver);
136 | padding: 8px 8px 16px 16px;
137 | height: 500px;
138 | width: 100%;
139 | overflow-y: auto;
140 | overflow-x: hidden;
141 |
142 | &--collapsed {
143 | display: none;
144 | }
145 | }
146 |
147 | .nodes {
148 | width: 100%;
149 | }
150 |
151 | .node {
152 | position: relative;
153 | text-align: left;
154 | border-left: 1px solid rgba(0,0,0,0.1);
155 | border-top-right-radius: 4px;
156 | border-bottom-right-radius: 4px;
157 | padding: 4px 0 0 5px;
158 | margin-left: 9px;
159 |
160 | &--top {
161 | border-left: none;
162 | padding-left: 0;
163 | margin-left: 0;
164 | margin-bottom: 24px;
165 | }
166 |
167 | .expand-collapse {
168 | margin-right: -6px;
169 | }
170 |
171 | .icon {
172 | width: 20px;
173 | height: 20px;
174 | background-position: center;
175 | overflow: hidden;
176 | }
177 |
178 | .title {
179 | font-weight: bold;
180 | display: flex;
181 | align-items: center;
182 | margin: 0;
183 | border-radius: 4px;
184 | pointer-events: none;
185 | max-width: 100%;
186 |
187 | &--collapsible {
188 | pointer-events: inherit;
189 | cursor: pointer;
190 | }
191 |
192 | .node-name {
193 | max-width: calc(100% - 36px);
194 | overflow: hidden;
195 | white-space: nowrap;
196 | text-overflow: ellipsis;
197 | }
198 |
199 | &:hover > .expand-collapse {
200 | opacity: 1;
201 | }
202 |
203 |
204 | }
205 |
206 | .props {
207 | width: 100%;
208 |
209 | .prop {
210 | display: flex;
211 | position: relative;
212 | align-items: center;
213 | padding: 2px 6px 2px 20px;
214 | background: #fff;
215 | opacity: .5;
216 | border-radius: 4px;
217 | margin: 0 8px 4px;
218 | max-width: fit-content;
219 | cursor: pointer;
220 | pointer-events: all;
221 |
222 | &::after {
223 | content: ' ';
224 | position: absolute;
225 | left: 4px;
226 | color: var(--purple);
227 | background: #eee;
228 | border-radius: 2px;
229 | height: 11px;
230 | width: 11px;
231 | line-height: 11px;
232 | box-shadow: 1px 1px 0px #888 inset, -1px -1px 0px #ddd inset;
233 | overflow: hidden;
234 | padding: 0 0 1px 1px;
235 |
236 | }
237 |
238 | &:hover::after {
239 | content: '';
240 | background: #ccc;
241 | }
242 |
243 | &.selected {
244 | opacity: 1;
245 | }
246 |
247 | &.selected::after {
248 | content: '✓';
249 | background: #fff;
250 | }
251 |
252 | &.selected:hover::after {
253 | content: '✓';
254 | background: #ddd;
255 | }
256 |
257 | // &--inline {
258 | // display: inline-flex;
259 | // }
260 |
261 | .prop-inner {
262 | display: flex;
263 | position: relative;
264 | align-items: center;
265 |
266 | // span:not(:first-child) {
267 | // margin-left: 2px;
268 | // }
269 | }
270 |
271 | .value {
272 | display: inline-flex;
273 | color: var(--purple);
274 | max-width: 70%;
275 | overflow: hidden;
276 | padding: 0 2px;
277 |
278 | .string {
279 | overflow: hidden;
280 | text-overflow: ellipsis;
281 | white-space: nowrap;
282 | }
283 |
284 | &--object {
285 | display: flex;
286 | flex-flow: column nowrap;
287 |
288 | .sub-prop {
289 | display: block;
290 | margin: 0;
291 |
292 | .sub-key {
293 | font-style: italic;
294 | margin-right: 4px;
295 | }
296 |
297 | .sub-value {
298 | font-weight: bold;
299 | }
300 | }
301 | }
302 |
303 | &--corners {
304 | border: 1px solid rgba(0,0,0,0.1);
305 | border-radius: 8px;
306 | width: auto;
307 | display: grid;
308 | grid-template-columns: auto auto;
309 | padding: 0;
310 |
311 | span {
312 | margin: 0 !important;
313 | padding: 2px 6px;
314 |
315 | &.right {
316 | text-align: right;
317 | }
318 | }
319 | }
320 |
321 | &--padding {
322 | border: 1px solid rgba(0,0,0,0.1);
323 | border-radius: 8px;
324 | width: auto;
325 | display: flex;
326 | flex-flow: row nowrap;
327 | padding: 0;
328 |
329 | span {
330 | margin: 0 !important;
331 | padding: 2px 6px;
332 | text-align: center;
333 | display: flex;
334 | flex-flow: column nowrap;
335 | justify-content: center;
336 | align-items: center;
337 |
338 | &.middle {
339 | padding: 0;
340 | }
341 |
342 | &.right {
343 | text-align: right;
344 | }
345 | }
346 | }
347 |
348 |
349 | }
350 |
351 |
352 | .key, .arrow {
353 | text-transform: capitalize;
354 | color: var(--hud)
355 | }
356 | }
357 |
358 | .color {
359 | display: inline-flex;
360 | align-items: center;
361 | position: relative;
362 | margin: 0 2px;
363 | }
364 |
365 | .rgbColor {
366 | display: inline-flex;
367 | align-items: center;
368 | position: relative;
369 | width: 13px;
370 | height: 13px;
371 | border: 1px solid #333;
372 | border-radius: 4px;
373 | margin-right: 2px;
374 |
375 | &--none {
376 | border-color: #ccc;
377 | }
378 |
379 | &--none::before {
380 | position: absolute;
381 | display: flex;
382 | overflow: hidden;
383 | align-items: center;
384 | justify-content: center;
385 | content: "╳";
386 | right: 0;
387 | bottom: 0;
388 | top: 0;
389 | left: 0;
390 | color: #999;
391 | }
392 |
393 | &--image {
394 | background:
395 | url('data:image/svg+xml;utf8,')
396 | 0 0/50% 50%;
397 | }
398 |
399 | &--gradient {
400 | background: linear-gradient(45deg, #666, #fff);
401 | }
402 | }
403 | }
404 | }
405 |
406 | // TODO: Implement tooltip that doesn't go outside page
407 | .hasTooltip {
408 | cursor: default;
409 | transition: all 0.15s;
410 | // box-shadow: 0 2px 4px rgba(0,0,0,0);
411 |
412 | &::after {
413 | display: flex;
414 | white-space: pre-line;
415 | content: attr(data-tooltip);
416 | position: absolute;
417 | font-size: var(--font-size-xsmall);
418 | bottom: 1.5em;
419 | // left: 50%;
420 | // transform: translateX(-50%);
421 | max-width: 100vw;
422 | background: #333;
423 | color: #fff;
424 | padding: 4px 8px;
425 | border-radius: 4px;
426 | opacity: 0;
427 | transition: opacity 0.15s;
428 | transition-delay: 0;
429 | pointer-events: none;
430 | }
431 |
432 | &:hover {
433 | // box-shadow: 0 2px 4px rgba(0,0,0,0.3);
434 | border-radius: 4px;
435 | z-index: 10;
436 |
437 | &::after {
438 | transition-delay: 0.25s;
439 | opacity: 1;
440 | }
441 | }
442 | }
443 |
444 | @keyframes spin {
445 | 0% {
446 | transform:rotate(0deg);
447 | }
448 | // 75% {
449 | // transform:rotate(360deg);
450 | // }
451 | 100% {
452 | transform:rotate(360deg);
453 | }
454 | }
455 |
456 | .loader {
457 | inset: 0;
458 | position: fixed;
459 | margin: auto;
460 | width: 64px;
461 | height: 64px;
462 | isolation: isolate;
463 |
464 | span {
465 | width: 68px;
466 | height: 68px;
467 | position: absolute;
468 | inset: 0;
469 | display: inline-block;
470 |
471 | &::before, &::after {
472 | content: '⚆';
473 | color: var(--purple);
474 | font-size: 60px;
475 | font-weight: 100;
476 | width: auto;
477 | height: 64px;
478 | border-radius: 8px;
479 | position: absolute;
480 | inset: 0;
481 | display: flex;
482 | justify-content: center;
483 | align-items: center;
484 | animation: spin 1.25s ease-in-out infinite;
485 | opacity: .5;
486 | }
487 | }
488 | span:first-child {
489 | &::before {
490 | animation-delay: 0;
491 | z-index: 4;
492 | opacity: 1;
493 | }
494 | &::after {
495 | filter: saturate(125%) brightness(125%) hue-rotate(45deg);
496 | animation-delay: 0.05s;
497 | z-index: 3;
498 | }
499 | }
500 | span:last-child {
501 | &::before {
502 | filter: saturate(150%) brightness(170%) hue-rotate(90deg);;
503 | animation-delay: 0.1s;
504 | z-index: 2;
505 | }
506 | &::after {
507 | filter: saturate(175%) brightness(250%) hue-rotate(135deg);
508 | animation-delay: 0.15s;
509 | z-index: 1;
510 | }
511 | }
512 |
513 |
514 | }
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import equal from 'deep-equal';
2 |
3 | const env = process.env.NODE_ENV;
4 |
5 | export function log(indentLevel:number = 0, ...args) {
6 | if (env !== "development") return;
7 | let indent:string = "──".repeat(indentLevel) + " ";
8 | console.log(indent, ...args);
9 | }
10 |
11 | // Recursive
12 | export function flattenData(data:IOverrideData):{} {
13 | let flat = {};
14 | if (!data) return flat;
15 | if (data.overriddenProps) {
16 | data.overriddenProps.map(prop => {
17 | flat[`${data.id}--${prop.key}`] = prop;
18 | });
19 | }
20 | if (data.children) {
21 | data.children.map(child => {
22 | flat = Object.assign({}, flat, flattenData(child));
23 | });
24 | }
25 | return flat;
26 | }
27 |
28 | // Override data associated with a node and its children
29 | export interface IOverrideData {
30 | sourceName: string;
31 | targetName: string;
32 | type: string;
33 | id: string; // Combo of source and target IDs
34 | sourceNode: SceneNode;
35 | targetNode: SceneNode;
36 | overriddenProps?: IOverrideProp[];
37 | children?: IOverrideData[];
38 | isCollapsed?: boolean;
39 | parentId?: string;
40 | }
41 |
42 | // An individual overridden property on a node. Values can be of many different types (see IProps)
43 | export interface IOverrideProp {
44 | key: string;
45 | sourceValue: any;
46 | targetValue: any;
47 | isApplied: boolean;
48 | }
49 |
50 | export interface IBoxSides {
51 | top: number;
52 | bottom: number;
53 | left: number;
54 | right: number;
55 | }
56 |
57 | export interface IBoxCorners {
58 | topLeft: number;
59 | topRight: number;
60 | bottomLeft: number;
61 | bottomRight: number;
62 | }
63 |
64 | export function getCombinedId(source:SceneNode, target:SceneNode):string {
65 | return `${source.id}__${target.id}`;
66 | }
67 |
68 | export function createOverrideData(sourceNode: SceneNode, targetNode: SceneNode): IOverrideData {
69 | let data: IOverrideData = {
70 | sourceName: sourceNode.name,
71 | targetName: targetNode.name,
72 | type: targetNode.type,
73 | id: getCombinedId(sourceNode, targetNode),
74 | sourceNode,
75 | targetNode,
76 | isCollapsed: true, // collapsed by default; recursively set to false if overrides exist
77 | };
78 | return data;
79 | }
80 |
81 | export interface IProps {
82 | width: number;
83 | height: number;
84 |
85 | // absoluteTransform: Transform;
86 | arcData: ArcData;
87 | // backgrounds: Paint[]; // Deprecated
88 | // backgroundStyleId: string; // Deprecated
89 | bottomLeftRadius: number;
90 | bottomRightRadius: number;
91 | blendMode: BlendMode;
92 | clipsContent: boolean;
93 | constrainProportions: boolean;
94 | constraints: Constraints;
95 | cornerRadius: number | PluginAPI["mixed"];
96 | corners: IBoxCorners; // Special case: constructed from other props
97 | cornerSmoothing: number;
98 | counterAxisSizingMode: string;
99 | counterAxisAlignItems: string;
100 | dashPattern: number[];
101 | effects: Effect[];
102 | effectStyleId: string;
103 | fills: Paint[] | PluginAPI["mixed"];
104 | fillStyleId: string | PluginAPI["mixed"];
105 | itemSpacing: number;
106 | isMask: boolean;
107 | layoutAlign: string;
108 | layoutGrow: number;
109 | layoutMode: string;
110 | locked: boolean;
111 | mainComponent: ComponentNode;
112 | name: string;
113 | opacity: number;
114 | padding: IBoxSides; // Special case: constructed from other props
115 | paddingBottom: number;
116 | paddingLeft: number;
117 | paddingRight: number;
118 | paddingTop: number;
119 | primaryAxisSizingMode: string;
120 | primaryAxisAlignItems: string;
121 | // relativeTransform: Transform;
122 | rotation: number;
123 | strokeAlign: string;
124 | strokeCap: string | PluginAPI["mixed"];
125 | strokeJoin: string | PluginAPI["mixed"];
126 | strokes: Paint[];
127 | strokeStyleId: string;
128 | topLeftRadius: number;
129 | topRightRadius: number;
130 | visible: boolean;
131 |
132 | characters: string;
133 | fontName: FontName | PluginAPI["mixed"];
134 | fontSize: number | PluginAPI["mixed"];
135 | letterSpacing: LetterSpacing | PluginAPI["mixed"];
136 | lineHeight: PluginAPI["mixed"] | any;
137 | paragraphIndent: number;
138 | paragraphSpacing: number;
139 | textAlignHorizontal: string;
140 | textAlignVertical: string;
141 | textAutoResize: string;
142 | textCase: string | PluginAPI["mixed"];
143 | textDecoration: string | PluginAPI["mixed"];
144 | textStyleId: string | PluginAPI["mixed"];
145 | }
146 |
147 | const parentTypes = [
148 | 'FRAME',
149 | 'GROUP',
150 | 'INSTANCE',
151 | 'COMPONENT',
152 | 'BOOLEAN_OPERATION'
153 | ]
154 |
155 | export function supportsChildren(node:any):boolean {
156 | return (parentTypes.indexOf(node.type) > -1);
157 | }
158 |
159 | const autoLayoutTypes = [
160 | 'FRAME',
161 | 'INSTANCE',
162 | 'COMPONENT'
163 | ]
164 |
165 | export function supportsAutoLayout(node:any):boolean {
166 | return (autoLayoutTypes.indexOf(node.type) > -1);
167 | }
168 |
169 | export function checkEquality(key: string, sourceValue: any, targetValue: any) {
170 | if (sourceValue === undefined && targetValue === undefined) return true;
171 | switch (key) {
172 | case "mainComponent":
173 | return sourceValue.id === targetValue.id;
174 | default:
175 | return equal(sourceValue, targetValue);
176 | }
177 | }
178 |
179 | export function formatOverrideValue(key: string, prop: any) {
180 | switch (key) {
181 | case "backgrounds":
182 | case "fills":
183 | case "strokes":
184 | if (!Array.isArray(prop) || !prop[0]) {
185 | return [];
186 | }
187 | break;
188 | case "mainComponent":
189 | return { name: prop.name, id: prop.id };
190 | }
191 | if (typeof prop === 'symbol') return '(Mixed)';
192 | return prop;
193 | }
194 |
195 | export function countChildren(node:any):number {
196 | let counter:number = 0;
197 | if (node.children) {
198 | node.children.forEach((child) => {
199 | counter++;
200 | counter += countChildren(child);
201 | })
202 | }
203 | return counter;
204 | }
205 |
206 |
207 |
208 | /**
209 | * NOTE: Using the explicit property names like this is many, many times
210 | * faster than an iterated string-based property lookup like node[key].
211 | */
212 | export function getPropsFromNode(node:any):IProps {
213 | let props:any = {};
214 |
215 | props.width = node.width;
216 | props.height = node.height;
217 |
218 | // props.absoluteTransform = node.absoluteTransform;
219 | props.arcData = node.arcData;
220 | // props.backgrounds = node.backgrounds; // Deprecated
221 | // props.backgroundStyleId = node.backgroundStyleId; // Deprecated
222 | props.blendMode = node.blendMode;
223 | props.bottomLeftRadius = node.bottomLeftRadius;
224 | props.bottomRightRadius = node.bottomRightRadius;
225 | props.clipsContent = node.clipsContent;
226 | props.constrainProportions = node.constrainProportions;
227 | props.constraints = node.constraints;
228 | props.cornerRadius = node.cornerRadius;
229 | props.cornerSmoothing = node.cornerSmoothing;
230 | props.counterAxisSizingMode = node.counterAxisSizingMode;
231 | props.counterAxisAlignItems = node.counterAxisAlignItems;
232 | props.dashPattern = node.dashPattern;
233 | props.effects = node.effects;
234 | props.effectStyleId = node.effectStyleId;
235 | props.fills = node.fills;
236 | props.fillStyleId = node.fillStyleId;
237 | props.isMask = node.isMask;
238 | props.itemSpacing = node.itemSpacing;
239 | props.layoutAlign = node.layoutAlign;
240 | props.layoutGrow = node.layoutGrow;
241 | props.layoutMode = node.layoutMode;
242 | props.locked = node.locked;
243 | props.mainComponent = node.mainComponent;
244 | props.name = node.name;
245 | props.opacity = node.opacity;
246 | props.paddingBottom = node.paddingBottom;
247 | props.paddingLeft = node.paddingLeft;
248 | props.paddingRight = node.paddingRight;
249 | props.paddingTop = node.paddingTop;
250 | props.primaryAxisSizingMode = node.primaryAxisSizingMode;
251 | props.primaryAxisAlignItems = node.primaryAxisAlignItems;
252 | props.rotation = node.rotation;
253 | // props.relativeTransform = node.relativeTransform;
254 | props.strokeAlign = node.strokeAlign;
255 | props.strokeCap = node.strokeCap;
256 | props.strokeJoin = node.strokeJoin;
257 | props.strokes = node.strokes;
258 | props.strokeStyleId = node.strokeStyleId;
259 | props.topLeftRadius = node.topLeftRadius;
260 | props.topRightRadius = node.topRightRadius;
261 | props.visible = node.visible;
262 |
263 | props.characters = node.characters;
264 | props.fontName = node.fontName;
265 | props.fontSize = node.fontSize;
266 | props.letterSpacing = node.letterSpacing;
267 | props.lineHeight = node.lineHeight;
268 | props.paragraphIndent = node.paragraphIndent
269 | props.paragraphSpacing = node.paragraphSpacing
270 | props.textAlignHorizontal = node.textAlignHorizontal;
271 | props.textAlignVertical = node.textAlignVertical;
272 | props.textAutoResize = node.textAutoResize;
273 | props.textCase = node.textCase;
274 | props.textDecoration = node.textDecoration;
275 | props.textStyleId = node.textStyleId;
276 |
277 | // Construct special-case objects
278 | props.padding = {
279 | top: node.paddingTop,
280 | bottom: node.paddingBottom,
281 | left: node.paddingLeft,
282 | right: node.paddingRight
283 | }
284 |
285 | if (typeof node.cornerRadius === 'symbol') {
286 | props.corners = {
287 | topLeft: node.topLeftRadius,
288 | topRight: node.topRightRadius,
289 | bottomLeft: node.bottomLeftRadius,
290 | bottomRight: node.bottomRightRadius
291 | }
292 | } else {
293 | props.corners = {
294 | topLeft: node.cornerRadius,
295 | topRight: node.cornerRadius,
296 | bottomLeft: node.cornerRadius,
297 | bottomRight: node.cornerRadius
298 | }
299 | }
300 |
301 | return props;
302 | }
303 |
304 | export enum SelectionValidation {
305 | NO_SELECTION = "Nothing selected",
306 | MORE_THAN_TWO = "More than two",
307 | IS_NODE = "Not an Instance",
308 | IS_INSTANCE = "One Instance",
309 | IS_TWO = "Two nodes",
310 | }
311 |
312 | export interface ISelectionValidation {
313 | isValid: boolean;
314 | reason?: SelectionValidation;
315 | childCount?: number;
316 | }
317 |
318 | export function validateSelection(selection: SceneNode[]): ISelectionValidation {
319 | if (selection.length === 0) {
320 | return { isValid: false, reason: SelectionValidation.NO_SELECTION };
321 | }
322 | if (selection.length > 2) {
323 | return { isValid: false, reason: SelectionValidation.MORE_THAN_TWO };
324 | }
325 | if (selection.length === 1) {
326 | let childCount = countChildren(selection[0]);
327 | if (selection[0].type !== "INSTANCE") {
328 | return { isValid: false, reason: SelectionValidation.IS_NODE, childCount };
329 | } else {
330 | return { isValid: true, reason: SelectionValidation.IS_INSTANCE, childCount };
331 | }
332 | }
333 | if (selection.length === 2) {
334 | let childCount = countChildren(selection[0]);
335 | return { isValid: true, reason: SelectionValidation.IS_TWO, childCount };
336 | }
337 | }
338 |
339 | export interface IColor {
340 | r: number;
341 | g: number;
342 | b: number;
343 | a?: number;
344 | }
345 |
346 | export function formatRgbaColor(color: IColor):IColor {
347 | let converted: IColor = {
348 | r: Math.round(color.r * 255),
349 | g: Math.round(color.g * 255),
350 | b: Math.round(color.b * 255)
351 | }
352 | if (color.a !== undefined) {
353 | converted.a = Math.round(color.a * 255);
354 | }
355 | return converted;
356 | }
357 |
358 | export function rgbaToHex(color: IColor) {
359 | let returnString:string;
360 | let base = 16;
361 | let r = color.r.toString(base);
362 | let g = color.g.toString(base);
363 | let b = color.b.toString(base);
364 | if (r.length == 1)
365 | r = "0" + r;
366 | if (g.length == 1)
367 | g = "0" + g;
368 | if (b.length == 1)
369 | b = "0" + b;
370 | returnString = "#" + r + g + b;
371 | if (color.a !== undefined) {
372 | let a = color.a.toString(base);
373 | if (a.length == 1)
374 | a = "0" + a;
375 | returnString += a;
376 | }
377 | return returnString.toUpperCase();
378 | }
379 |
380 | export function createCssGradient(paint:GradientPaint) {
381 | let gradient:string = "";
382 | switch (paint.type) {
383 | default:
384 | case "GRADIENT_LINEAR":
385 | gradient += "linear-gradient(";
386 | break;
387 | case "GRADIENT_RADIAL":
388 | gradient += "radial-gradient(";
389 | break;
390 | case "GRADIENT_ANGULAR":
391 | case "GRADIENT_DIAMOND":
392 | gradient += "conic-gradient(";
393 | break;
394 | }
395 | paint.gradientStops.forEach(stop => {
396 | let formatted = formatRgbaColor(stop.color);
397 | let stopString:string = rgbaToHex(formatted);
398 | gradient += stopString + ", "
399 | })
400 | gradient = gradient.slice(0, -2);
401 | gradient += ")";
402 | return gradient;
403 | }
404 |
405 | export function truncate(value:any, chars:number = 0) {
406 | if (!isNaN(value)) {
407 | return parseFloat((value as number).toFixed(chars));
408 | } else {
409 | return value;
410 | }
411 | }
412 |
413 | export function deCamel(s:string):string {
414 | const regex = s.replace(/([A-Z]{1,})/g, " $1");
415 | return regex.charAt(0).toUpperCase() + regex.slice(1);
416 | }
417 |
--------------------------------------------------------------------------------
/src/code.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IOverrideData,
3 | IOverrideProp,
4 | checkEquality,
5 | createOverrideData,
6 | formatOverrideValue,
7 | getPropsFromNode,
8 | log,
9 | supportsAutoLayout,
10 | supportsChildren,
11 | validateSelection,
12 | } from "./util";
13 |
14 | figma.showUI(__html__, { title: "Maximum Override", width: 540, height: 600 });
15 |
16 | let temporaryNodes: SceneNode[] = [];
17 | let dataById: { [id: string]: IOverrideData };
18 | let comparedNodeCount: number = 0;
19 | let hasOverrides: IOverrideData[] = [];
20 |
21 | /**
22 | * @param data The node that's being recursively introspected
23 | * @param recursionLevel Integer for indenting output strings
24 | */
25 | function getOverridesForData(
26 | data:IOverrideData,
27 | recursionLevel: number
28 | ):IOverrideData {
29 | let {sourceNode, targetNode} = data;
30 | // let start = new Date().getTime();
31 | if (targetNode.type !== sourceNode.type) {
32 | log(recursionLevel, `Can't compare ${sourceNode.type} to ${targetNode.type}`);
33 | return null;
34 | }
35 | let overriddenProps = [];
36 | let targetProps = getPropsFromNode(targetNode);
37 | let sourceProps = getPropsFromNode(sourceNode);
38 | for (const key in targetProps) {
39 | if (!checkEquality(key, targetProps[key], sourceProps[key])) {
40 | let prop: IOverrideProp = {
41 | key: key,
42 | sourceValue: formatOverrideValue(key, sourceProps[key]),
43 | targetValue: formatOverrideValue(key, targetProps[key]),
44 | isApplied: true,
45 | };
46 | overriddenProps.push(prop);
47 | }
48 | }
49 | data.overriddenProps = overriddenProps;
50 | return data;
51 | }
52 |
53 | // Recursive
54 | function compareProps(
55 | data: IOverrideData,
56 | recursionLevel: number
57 | ):IOverrideData {
58 | data.overriddenProps = []; // Clear in case of swap
59 | data = getOverridesForData(data, recursionLevel + 1);
60 | if (!data || !data.overriddenProps) return;
61 | if (data.overriddenProps.length > 0) {
62 | hasOverrides.push(data);
63 | }
64 | const targetNode:any = data.targetNode;
65 | /*
66 | If the target's mainComponent has changed, we need to compare against an instance
67 | of that main rather than the original component. Note that this only applies to
68 | nested instances within the target, not the root level, hence recursionLevel > 1.
69 | TODO: make this toggleable?
70 | */
71 | let hasNewMain: boolean;
72 | data.overriddenProps.forEach((prop) => {
73 | if (prop.key === "mainComponent" && recursionLevel > 1) {
74 | hasNewMain = true;
75 | }
76 | });
77 | if (hasNewMain) {
78 | let component = (data.targetNode as InstanceNode).mainComponent as ComponentNode;
79 | if (component.remote) {
80 | log(recursionLevel + 1, "Remote component detected. Key:", component.key);
81 | }
82 | try {
83 | let newSourceNode: SceneNode = component.createInstance();
84 | let newSourceData: IOverrideData = createOverrideData(newSourceNode, targetNode);
85 | log(recursionLevel, "New Main", newSourceNode.name, newSourceData);
86 | temporaryNodes.push(newSourceNode);
87 | } catch (e) {
88 | log(recursionLevel + 1, "Couldn't create an instance. Is this a nested main component?", e );
89 | return;
90 | }
91 | }
92 |
93 | if (supportsChildren(targetNode)) {
94 | const targetChildren = targetNode.children;
95 | let dataChildren:IOverrideData[] = [];
96 | for (let i = 0, n = targetChildren.length; i < n; i++) {
97 | const targetChild = targetChildren[i] as SceneNode;
98 | const sourceNode:any = data.sourceNode;
99 | if (supportsChildren(sourceNode)) {
100 | const sourceChild = (sourceNode as any).children[i];
101 | if (sourceChild === undefined) {
102 | log(recursionLevel + 1, `sourceChild at ${i} is undefined in ${sourceNode.children}`);
103 | break;
104 | }
105 | const childData = createOverrideData(sourceChild, targetChild);
106 | childData.parentId = data.id;
107 | dataChildren.push(compareProps(childData, recursionLevel + 1));
108 | } else {
109 | log(recursionLevel + 1, `Original node ${data.sourceName} has no matching children.`);
110 | }
111 | }
112 | data.children = dataChildren.length > 0 ? dataChildren : null;
113 | }
114 |
115 | comparedNodeCount++;
116 | dataById[data.id] = data;
117 | return data;
118 | }
119 |
120 | function cleanUpTemporaryNodes() {
121 | temporaryNodes.forEach((node) => {
122 | node.remove();
123 | });
124 | temporaryNodes = [];
125 | }
126 |
127 | function getDataFromSelection(selection: SceneNode[]): any {
128 | let targetNode: SceneNode, sourceNode: SceneNode;
129 | let data: IOverrideData;
130 | if (selection.length === 1) {
131 | targetNode = selection[0];
132 | if (targetNode.type === "INSTANCE") {
133 | targetNode = selection[0] as InstanceNode;
134 | sourceNode = targetNode.mainComponent.createInstance();
135 | data = createOverrideData(sourceNode, targetNode);
136 | data.sourceName = "Main";
137 | log(0, `Comparing main ${data.sourceName} to target ${data.targetName}`);
138 | temporaryNodes.push(data.sourceNode);
139 | } else {
140 | log(0, "Selection must be an Instance.");
141 | }
142 | } else if (selection.length == 2) {
143 | // note that order is reversed
144 | sourceNode = selection[0];
145 | targetNode = selection[1];
146 | data = createOverrideData(sourceNode, targetNode);
147 | } else {
148 | log(0, "Cannot compare more than two selected items.");
149 | }
150 | return data;
151 | }
152 |
153 | // Recursive (in reverse)
154 | function expandParents(data: IOverrideData) {
155 | data.isCollapsed = false;
156 | if (data.parentId) {
157 | const parent = dataById[data.parentId];
158 | // if the parent has already been expanded, stop here
159 | if (parent.isCollapsed) expandParents(parent);
160 | }
161 | }
162 |
163 | function getOverrides(
164 | data: IOverrideData
165 | ) {
166 | hasOverrides = [];
167 | dataById = {};
168 | data = compareProps(data, 1);
169 | hasOverrides.forEach((data) => expandParents(data));
170 | cleanUpTemporaryNodes();
171 | return data;
172 | }
173 |
174 | /******************************************
175 | * Apply overrides
176 | ******************************************/
177 |
178 | /**
179 | * @param key Override property key as string
180 | * @param prop The prop data object
181 | * @param target The Figma node to apply the prop to
182 | * @param isRoot Whether this is the top-level parent
183 | * @returns true if we're updating the main component
184 | */
185 | function applyOverrideProp(
186 | key: string,
187 | prop: any,
188 | target: SceneNode,
189 | isRoot: boolean
190 | ): boolean {
191 | let textProps: any[] = [];
192 | switch (key) {
193 | case "backgrounds":
194 | case "fills":
195 | case "strokes":
196 | if (Array.isArray(prop)) {
197 | target[key] = prop;
198 | }
199 | return false;
200 | case "mainComponent":
201 | // don't apply this one at the root level
202 | if (!isRoot) {
203 | //TODO: Add checkbox for "don't rename layers"
204 | // target["autoRename"] = false;
205 | target[key] = prop;
206 | }
207 | return true;
208 | // For these text props, we must async load the font first.
209 | // Applying them should be almost synchronous once the font has been loaded.
210 | case "fontName":
211 | let textNode = target as TextNode;
212 | let fontName: FontName = prop as FontName;
213 | figma.loadFontAsync(fontName).then((data) => {
214 | (textNode as any).fontName = fontName;
215 | });
216 | return false;
217 | case "characters":
218 | case "fontSize":
219 | case "letterSpacing":
220 | case "lineHeight":
221 | case "paragraphIndent":
222 | case "paragraphSpacing":
223 | case "paragraphSpacing":
224 | case "textAlignHorizontal":
225 | case "textAlignVertical":
226 | case "textAutoResize":
227 | case "textCase":
228 | case "textDecoration":
229 | case "textStyleId":
230 | //TODO: Send UI notifications for errors
231 | if (key in target) {
232 | if (typeof prop === "symbol") {
233 | log(0, "Multiple font attributes detected within ", prop);
234 | return false;
235 | }
236 | let textNode = target as TextNode;
237 | if (textNode.hasMissingFont) {
238 | log(0, "Text field has missing font. Can't edit properties.");
239 | return false;
240 | }
241 | textProps.push({ key, prop });
242 | let fontName = textNode.fontName;
243 | if (typeof fontName === "symbol") {
244 | log(0, "Multiple font attributes detected within ", target.name);
245 | return false;
246 | }
247 | figma.loadFontAsync(fontName).then((data) => {
248 | (textNode as any)[key] = prop;
249 | });
250 | }
251 | return false;
252 | //TODO: Only call resize() once
253 | case "width":
254 | target.resize(prop, target.height);
255 | return false;
256 | case "height":
257 | target.resize(target.width, prop);
258 | return false;
259 | case "x":
260 | case "y":
261 | const hasAutoLayout =
262 | supportsAutoLayout(target) && (target as any).layoutAlign !== "NONE";
263 | if (!hasAutoLayout && !isRoot) {
264 | target[key] = prop;
265 | }
266 | default:
267 | target[key] = prop;
268 | return false;
269 | }
270 | }
271 |
272 | // Recursive
273 | function applyOverridesToNode(
274 | data: IOverrideData,
275 | target: any,
276 | recursionLevel: number
277 | ) {
278 | const isRoot = recursionLevel === 1;
279 | log(recursionLevel, "applyOverridesToNode", data.targetName);
280 | data.overriddenProps.forEach((prop) => {
281 | try {
282 | if (prop.key in target) {
283 | if (prop.isApplied) {
284 | applyOverrideProp(prop.key, prop.sourceValue, target, isRoot);
285 | }
286 | }
287 | } catch (e) {
288 | log(0, "Cannot apply prop", prop, e);
289 | }
290 | });
291 | // recursion
292 | if (data.children) {
293 | for (let i: number = 0; i < data.children.length; i++) {
294 | const childData = data.children[i];
295 | // find child element by corresponding position in hierarchy
296 | let targetChild: SceneNode;
297 | try {
298 | targetChild = target.children[i] as SceneNode;
299 | applyOverridesToNode(childData, targetChild, recursionLevel + 1);
300 | } catch (e) {
301 | log(0, "Error", e, "in applyOverridesToNode.");
302 | log(0, " targetChild:", targetChild);
303 | log(0, " in", target.children, "at", i, "on parent", target);
304 | }
305 | }
306 | }
307 | }
308 |
309 | /*******************************************************
310 | * Handle UI
311 | *******************************************************/
312 |
313 | async function validateClientStorage() {
314 | const data = await figma.clientStorage.getAsync("copiedOverrides");
315 | const isValid = data !== undefined;
316 | if (isValid) {
317 | figma.ui.postMessage({
318 | type: "client-storage-validated",
319 | });
320 | }
321 | return isValid;
322 | }
323 |
324 | figma.on("selectionchange", () => {
325 | const selection: SceneNode[] = Array.from(figma.currentPage.selection);
326 | const validation = validateSelection(selection);
327 | validateClientStorage()
328 | .then((isValid) => {
329 | figma.ui.postMessage({
330 | type: "selection-validation",
331 | validation,
332 | clientStorageIsValid: isValid,
333 | });
334 | })
335 | .catch(() => {
336 | // TODO: Notify client
337 | figma.ui.postMessage({
338 | type: "selection-validation",
339 | validation,
340 | clientStorageIsValid: false,
341 | });
342 | });
343 | });
344 |
345 | figma.ui.onmessage = (msg) => {
346 | if (msg.type === "initial-render") {
347 | const selection: SceneNode[] = Array.from(figma.currentPage.selection);
348 | figma.ui.postMessage({
349 | type: "selection-validation",
350 | validation: validateSelection(selection),
351 | });
352 | validateClientStorage();
353 | }
354 |
355 | if (msg.type === "compare-selected") {
356 | const selection: SceneNode[] = Array.from(figma.currentPage.selection);
357 | let start = new Date().getTime();
358 | log(0, "Started inspecting selected nodes...")
359 | let data = getDataFromSelection(selection);
360 | let diff = getOverrides(data);
361 | let end = new Date().getTime();
362 | log(1, "Finished inspecting selected nodes.", data.id, end - start);
363 | figma.ui.postMessage({
364 | type: "comparison-finished",
365 | payload: diff,
366 | });
367 | }
368 |
369 | if (msg.type === "copy-overrides") {
370 | log(0, "Received override data. Saving...", msg);
371 | let data: IOverrideData = msg.data;
372 | figma.clientStorage.setAsync("copiedOverrides", data);
373 | figma.ui.postMessage({ type: "copy-confirmation" });
374 | }
375 |
376 | if (msg.type === "paste-overrides") {
377 | log(0, "Received paste request. Getting node...", msg);
378 | let target: SceneNode;
379 | if (figma.currentPage.selection.length === 1) {
380 | target = figma.currentPage.selection[0] as SceneNode;
381 | } else {
382 | if (msg.data.targetId) {
383 | target = figma.getNodeById(msg.data.targetId) as SceneNode;
384 | }
385 | }
386 | if (target === undefined) {
387 | target = figma.currentPage.selection[0] as SceneNode;
388 | }
389 | figma.clientStorage
390 | .getAsync("copiedOverrides")
391 | .then((data) => {
392 | log(0, "got async data", data, "target", target);
393 | applyOverridesToNode(data, target, 1);
394 | })
395 | .catch((error) => {
396 | // TODO: post notification
397 | log(0, "ERROR: async", error);
398 | })
399 | .finally(() => {});
400 | }
401 |
402 | if (msg.type === "swap-selected") {
403 | log(0, "Swapping symbols", msg);
404 | let originalData:IOverrideData = createOverrideData(
405 | figma.getNodeById(msg.data.targetId) as SceneNode,
406 | figma.getNodeById(msg.data.sourceId) as SceneNode
407 | );
408 | let diff = getOverrides(
409 | originalData,
410 | );
411 | log(0, "Finished inspecting swapped nodes.", originalData.id);
412 | figma.ui.postMessage({
413 | type: "comparison-finished",
414 | payload: diff,
415 | });
416 | }
417 |
418 | if (msg.type === "expand-ui") {
419 | figma.ui.resize(540, 100);
420 | }
421 |
422 | if (msg.type === "collapse-ui") {
423 | figma.ui.resize(540, 600);
424 | }
425 |
426 | if (msg.type === "prop-click") {
427 | log(0, `Prop "${msg.propId}" clicked on node ${msg.nodeId}`);
428 | }
429 | };
430 |
--------------------------------------------------------------------------------
/dist/ui.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/ui.css","webpack:///./node_modules/css-loader/dist/runtime/api.js","webpack:///./node_modules/style-loader/lib/addStyles.js","webpack:///./node_modules/style-loader/lib/urls.js","webpack:///./src/ui.css?3b97","webpack:///./src/ui.ts"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,kDAA0C,gCAAgC;AAC1E;AACA;;AAEA;AACA;AACA;AACA,gEAAwD,kBAAkB;AAC1E;AACA,yDAAiD,cAAc;AAC/D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAyC,iCAAiC;AAC1E,wHAAgH,mBAAmB,EAAE;AACrI;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;;AAGA;AACA;;;;;;;;;;;;AClFA,2BAA2B,mBAAO,CAAC,qGAAgD;AACnF;AACA,cAAc,QAAS,SAAS,0BAA0B,uBAAuB,iBAAiB,GAAG,YAAY,kBAAkB,6BAA6B,GAAG,UAAU,uBAAuB,sBAAsB,iBAAiB,iBAAiB,sBAAsB,gBAAgB,sCAAsC,kBAAkB,GAAG,WAAW,qBAAqB,wBAAwB,iBAAiB,GAAG,SAAS,iBAAiB,kBAAkB,iBAAiB,GAAG,eAAe,gDAAgD,EAAE,gBAAgB,qCAAqC,EAAE,iBAAiB,gDAAgD,EAAE,eAAe,qCAAqC,EAAE;;;;;;;;;;;;;ACF7sB;;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB;;AAEhB;AACA;AACA;;AAEA;AACA,2CAA2C,qBAAqB;AAChE;;AAEA;AACA,KAAK;AACL,IAAI;AACJ;;;AAGA;AACA;AACA;AACA;AACA;;AAEA;;AAEA,mBAAmB,iBAAiB;AACpC;AACA;;AAEA;AACA;AACA;AACA;;AAEA,oBAAoB,qBAAqB;AACzC,6BAA6B;AAC7B;AACA;AACA;;AAEA;AACA;AACA;AACA,SAAS;AACT;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,8BAA8B;;AAE9B;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA,CAAC;;;AAGD;AACA;AACA;AACA,qDAAqD,cAAc;AACnE;AACA,C;;;;;;;;;;;ACzFA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,8CAA8C;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA;;AAEA,cAAc,mBAAO,CAAC,uDAAQ;;AAE9B;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AAEA;;AAEA;AACA;;AAEA,iBAAiB,mBAAmB;AACpC;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA,iBAAiB,sBAAsB;AACvC;;AAEA;AACA,mBAAmB,2BAA2B;;AAE9C;AACA;AACA;AACA;AACA;;AAEA;AACA,gBAAgB,mBAAmB;AACnC;AACA;;AAEA;AACA;;AAEA,iBAAiB,2BAA2B;AAC5C;AACA;;AAEA,QAAQ,uBAAuB;AAC/B;AACA;AACA,GAAG;AACH;;AAEA,iBAAiB,uBAAuB;AACxC;AACA;;AAEA,2BAA2B;AAC3B;AACA;AACA;;AAEA;AACA;AACA;;AAEA,gBAAgB,iBAAiB;AACjC;AACA;AACA;AACA;AACA;AACA,cAAc;;AAEd,kDAAkD,sBAAsB;AACxE;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA,GAAG;AACH;AACA,GAAG;AACH;AACA;AACA;AACA,EAAE;AACF;AACA,EAAE;AACF;AACA;AACA,EAAE;AACF;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,EAAE;AACF;;AAEA;AACA,KAAK,KAAwC,EAAE,EAE7C;;AAEF,QAAQ,sBAAiB;AACzB;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;;AAEA,EAAE;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,GAAG;AACH;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,CAAC;;AAED;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;;AAEA;;AAEA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uDAAuD;AACvD;;AAEA,6BAA6B,mBAAmB;;AAEhD;;AAEA;;AAEA;AACA;;;;;;;;;;;;;AC9YA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,wCAAwC,WAAW,EAAE;AACrD,wCAAwC,WAAW,EAAE;;AAErD;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,GAAG;AACH;AACA,sCAAsC;AACtC,GAAG;AACH;AACA,8DAA8D;AAC9D;;AAEA;AACA;AACA,EAAE;;AAEF;AACA;AACA;;;;;;;;;;;;;ACvFA,cAAc,mBAAO,CAAC,4GAAmD;;AAEzE,4CAA4C,QAAS;;AAErD;AACA;;;;AAIA,eAAe;;AAEf;AACA;;AAEA,aAAa,mBAAO,CAAC,mGAAgD;;AAErE;;AAEA,GAAG,KAAU,EAAE,E;;;;;;;;;;;;ACnBf;AAAA;AAAA;AAAkB;AAClB;AACA,wBAAwB,iBAAiB,qBAAqB,EAAE;AAChE;AACA;AACA,wBAAwB,iBAAiB,sBAAsB,EAAE;AACjE;AACA;AACA,oBAAoB,iBAAiB,qBAAqB,EAAE","file":"ui.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/ui.ts\");\n","exports = module.exports = require(\"../node_modules/css-loader/dist/runtime/api.js\")(false);\n// Module\nexports.push([module.id, \"body {\\n font: 12px sans-serif;\\n text-align: center;\\n margin: 20px;\\n}\\n.buttons {\\n display: flex;\\n flex-flow: column nowrap;\\n}\\nbutton {\\n border-radius: 5px;\\n background: white;\\n color: black;\\n border: none;\\n padding: 8px 15px;\\n margin: 2px;\\n box-shadow: inset 0 0 0 1px black;\\n outline: none;\\n}\\n#create {\\n box-shadow: none;\\n background: #18A0FB;\\n color: white;\\n}\\ninput {\\n border: none;\\n outline: none;\\n padding: 8px;\\n}\\ninput:hover { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); }\\nbutton:focus { box-shadow: inset 0 0 0 2px #18A0FB; }\\n#create:focus { box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.3); }\\ninput:focus { box-shadow: inset 0 0 0 2px #18A0FB; }\\n\", \"\"]);\n","\"use strict\";\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\n// css base code, injected by the css-loader\n// eslint-disable-next-line func-names\nmodule.exports = function (useSourceMap) {\n var list = []; // return the list of modules as css string\n\n list.toString = function toString() {\n return this.map(function (item) {\n var content = cssWithMappingToString(item, useSourceMap);\n\n if (item[2]) {\n return \"@media \".concat(item[2], \"{\").concat(content, \"}\");\n }\n\n return content;\n }).join('');\n }; // import a list of modules into the list\n // eslint-disable-next-line func-names\n\n\n list.i = function (modules, mediaQuery) {\n if (typeof modules === 'string') {\n // eslint-disable-next-line no-param-reassign\n modules = [[null, modules, '']];\n }\n\n var alreadyImportedModules = {};\n\n for (var i = 0; i < this.length; i++) {\n // eslint-disable-next-line prefer-destructuring\n var id = this[i][0];\n\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n\n for (var _i = 0; _i < modules.length; _i++) {\n var item = modules[_i]; // skip already imported module\n // this implementation is not 100% perfect for weird media query combinations\n // when a module is imported multiple times with different media queries.\n // I hope this will never occur (Hey this way we have smaller bundles)\n\n if (item[0] == null || !alreadyImportedModules[item[0]]) {\n if (mediaQuery && !item[2]) {\n item[2] = mediaQuery;\n } else if (mediaQuery) {\n item[2] = \"(\".concat(item[2], \") and (\").concat(mediaQuery, \")\");\n }\n\n list.push(item);\n }\n }\n };\n\n return list;\n};\n\nfunction cssWithMappingToString(item, useSourceMap) {\n var content = item[1] || ''; // eslint-disable-next-line prefer-destructuring\n\n var cssMapping = item[3];\n\n if (!cssMapping) {\n return content;\n }\n\n if (useSourceMap && typeof btoa === 'function') {\n var sourceMapping = toComment(cssMapping);\n var sourceURLs = cssMapping.sources.map(function (source) {\n return \"/*# sourceURL=\".concat(cssMapping.sourceRoot).concat(source, \" */\");\n });\n return [content].concat(sourceURLs).concat([sourceMapping]).join('\\n');\n }\n\n return [content].join('\\n');\n} // Adapted from convert-source-map (MIT)\n\n\nfunction toComment(sourceMap) {\n // eslint-disable-next-line no-undef\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n return \"/*# \".concat(data, \" */\");\n}","/*\n\tMIT License http://www.opensource.org/licenses/mit-license.php\n\tAuthor Tobias Koppers @sokra\n*/\n\nvar stylesInDom = {};\n\nvar\tmemoize = function (fn) {\n\tvar memo;\n\n\treturn function () {\n\t\tif (typeof memo === \"undefined\") memo = fn.apply(this, arguments);\n\t\treturn memo;\n\t};\n};\n\nvar isOldIE = memoize(function () {\n\t// Test for IE <= 9 as proposed by Browserhacks\n\t// @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805\n\t// Tests for existence of standard globals is to allow style-loader\n\t// to operate correctly into non-standard environments\n\t// @see https://github.com/webpack-contrib/style-loader/issues/177\n\treturn window && document && document.all && !window.atob;\n});\n\nvar getTarget = function (target, parent) {\n if (parent){\n return parent.querySelector(target);\n }\n return document.querySelector(target);\n};\n\nvar getElement = (function (fn) {\n\tvar memo = {};\n\n\treturn function(target, parent) {\n // If passing function in options, then use it for resolve \"head\" element.\n // Useful for Shadow Root style i.e\n // {\n // insertInto: function () { return document.querySelector(\"#foo\").shadowRoot }\n // }\n if (typeof target === 'function') {\n return target();\n }\n if (typeof memo[target] === \"undefined\") {\n\t\t\tvar styleTarget = getTarget.call(this, target, parent);\n\t\t\t// Special case to return head of iframe instead of iframe itself\n\t\t\tif (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {\n\t\t\t\ttry {\n\t\t\t\t\t// This will throw an exception if access to iframe is blocked\n\t\t\t\t\t// due to cross-origin restrictions\n\t\t\t\t\tstyleTarget = styleTarget.contentDocument.head;\n\t\t\t\t} catch(e) {\n\t\t\t\t\tstyleTarget = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tmemo[target] = styleTarget;\n\t\t}\n\t\treturn memo[target]\n\t};\n})();\n\nvar singleton = null;\nvar\tsingletonCounter = 0;\nvar\tstylesInsertedAtTop = [];\n\nvar\tfixUrls = require(\"./urls\");\n\nmodule.exports = function(list, options) {\n\tif (typeof DEBUG !== \"undefined\" && DEBUG) {\n\t\tif (typeof document !== \"object\") throw new Error(\"The style-loader cannot be used in a non-browser environment\");\n\t}\n\n\toptions = options || {};\n\n\toptions.attrs = typeof options.attrs === \"object\" ? options.attrs : {};\n\n\t// Force single-tag solution on IE6-9, which has a hard limit on the # of