From 6b3b5aeb0ac92b1f0f6fb0394ca2810409032b44 Mon Sep 17 00:00:00 2001 From: limitlessloop <5551+limitlessloop@users.noreply.github.com> Date: Thu, 17 Dec 2020 19:27:07 +0000 Subject: [PATCH 1/2] Added copyPasteProps helper --- __tests__/copyPasteProps.test.ts | 67 ++++++++++++ src/helpers/copyPasteProps.ts | 177 +++++++++++++++++++++++++++++++ src/index.ts | 2 + 3 files changed, 246 insertions(+) create mode 100644 __tests__/copyPasteProps.test.ts create mode 100644 src/helpers/copyPasteProps.ts diff --git a/__tests__/copyPasteProps.test.ts b/__tests__/copyPasteProps.test.ts new file mode 100644 index 0000000..218fb6b --- /dev/null +++ b/__tests__/copyPasteProps.test.ts @@ -0,0 +1,67 @@ +import { copyPasteProps } from '../src' + +describe('copyPasteNode', () => { + test('copy compatible properties', () => { + const source = { + fills: 1, + fillStyleId: 'soaowlqla', + strokes: 1, + notAllowed: 1 + } + + const target = { + fills: 0, + fillStyleId: '', + strokes: 0, + backgrounds: [] + } + expect(copyPasteProps({ source, target })).toEqual({ + fills: 0, + fillStyleId: 'soaowlqla', + strokes: 1, + backgrounds: [] + }) + }) + test('exclude certain properties from being copied', () => { + const source = { + fills: 1, + fillStyleId: 'soaowlqla', + strokes: 1, + notAllowed: 1 + } + + const target = { + fills: 0, + fillStyleId: '', + strokes: 0, + backgrounds: [] + } + expect(copyPasteProps({ source, target, exclude: ['strokes'] })).toEqual({ + fills: 0, + fillStyleId: 'soaowlqla', + strokes: 0, + backgrounds: [] + }) + }) + test('only copy certain properties', () => { + const source = { + fills: 1, + fillStyleId: 'soaowlqla', + strokes: 1, + notAllowed: 1 + } + + const target = { + fills: 0, + fillStyleId: '', + strokes: 0, + backgrounds: [] + } + expect(copyPasteProps({ source, target, include: ['strokes'] })).toEqual({ + fills: 0, + fillStyleId: '', + strokes: 1, + backgrounds: [] + }) + }) +}) diff --git a/src/helpers/copyPasteProps.ts b/src/helpers/copyPasteProps.ts new file mode 100644 index 0000000..d19bd1f --- /dev/null +++ b/src/helpers/copyPasteProps.ts @@ -0,0 +1,177 @@ +/** + * Copy properties from one node to another while avoiding conflicts. When no target node is provided it returns a new object. + * + * For example: + * ```js + * const rectangle = figma.createRectangle() + * const frame = figma.createFrame() + * + * copyPaste({ rectangle, frame, exclude: ['fills'] }) + * ``` + * + * This will copy and paste all properties except for `fills` and readonly properties. + * + * @param source - Node being copied from + * @param target - Node being copied to + * @param include - Props that should be copied + * @param exclude - Props that shouldn't be copied + */ + +interface Arguments { + source: {} + target?: {} + include?: string[] + exclude?: string[] +} + +const nodeProps: string[] = [ + 'id', + 'parent', + 'name', + 'removed', + 'visible', + 'locked', + 'children', + 'constraints', + 'absoluteTransform', + 'relativeTransform', + 'x', + 'y', + 'rotation', + 'width', + 'height', + 'constrainProportions', + 'layoutAlign', + 'layoutGrow', + 'opacity', + 'blendMode', + 'isMask', + 'effects', + 'effectStyleId', + 'expanded', + 'backgrounds', + 'backgroundStyleId', + 'fills', + 'strokes', + 'strokeWeight', + 'strokeMiterLimit', + 'strokeAlign', + 'strokeCap', + 'strokeJoin', + 'dashPattern', + 'fillStyleId', + 'strokeStyleId', + 'cornerRadius', + 'cornerSmoothing', + 'topLeftRadius', + 'topRightRadius', + 'bottomLeftRadius', + 'bottomRightRadius', + 'exportSettings', + 'overflowDirection', + 'numberOfFixedChildren', + 'overlayPositionType', + 'overlayBackground', + 'overlayBackgroundInteraction', + 'reactions', + 'description', + 'remote', + 'key', + 'layoutMode', + 'primaryAxisSizingMode', + 'counterAxisSizingMode', + 'primaryAxisAlignItems', + 'counterAxisAlignItems', + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'itemSpacing', + 'horizontalPadding', + 'verticalPadding', + 'layoutGrids', + 'gridStyleId', + 'clipsContent', + 'guides' +] + +const readonly: string[] = [ + 'id', + 'parent', + 'removed', + 'children', + 'absoluteTransform', + 'width', + 'height', + 'overlayPositionType', + 'overlayBackground', + 'overlayBackgroundInteraction', + 'reactions', + 'remote', + 'key', + 'type' +] + +export default function copyPasteProps({ source, target, include, exclude }: Arguments) { + let allowlist: string[] = nodeProps + + if (include) { + allowlist = include + } else if (exclude) { + allowlist = allowlist.filter(function(el) { + return !exclude.concat(readonly).includes(el) + }) + } + + const val = source + const type = typeof source + + if ( + type === 'undefined' || + type === 'number' || + type === 'string' || + type === 'boolean' || + type === 'symbol' || + source === null + ) { + return val + } else if (type === 'object') { + if (val instanceof Array) { + return val.map(copyPasteProps) + } else if (val instanceof Uint8Array) { + return new Uint8Array(val) + } else { + const o: any = {} + for (const key1 in val) { + if (target) { + for (const key2 in target) { + if (allowlist.includes(key2)) { + if (key1 === key2) { + o[key1] = copyPasteProps({ + source: val[key1], + target, + include, + exclude + }) + } + } + } + } else { + if (allowlist.includes(key1)) { + o[key1] = copyPasteProps({ source: val[key1], include, exclude }) + } + } + } + + if (target) { + !o.fillStyleId && o.fills ? null : delete o.fills + !o.strokeStyleId && o.strokes ? null : delete o.strokes + !o.backgroundStyleId && o.backgrounds ? null : delete o.backgrounds + } + + return target ? Object.assign(target, o) : o + } + } + + throw 'unknown' +} diff --git a/src/index.ts b/src/index.ts index 623d23b..06e003e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ //import all helper functions here import clone from './helpers/clone' +import copyPasteProps from './helpers/copyPasteProps' import getAllFonts from './helpers/getAllFonts' import getBoundingRect from './helpers/getBoundingRect' import getNodeIndex from './helpers/getNodeIndex' @@ -62,6 +63,7 @@ export { isVisibleNode, isOneOfNodeType, clone, + copyPasteProps, getBoundingRect, nodeToObject, getTextNodeCSS, From c38e1c1520e7d3b0f6a54ead85adb017d8653486 Mon Sep 17 00:00:00 2001 From: limitlessloop <5551+limitlessloop@users.noreply.github.com> Date: Fri, 18 Dec 2020 17:49:16 +0000 Subject: [PATCH 2/2] Fixed recursion issue and changed function signiture. --- __tests__/copyPasteProps.test.ts | 6 +++--- src/helpers/copyPasteProps.ts | 26 +++++++------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/__tests__/copyPasteProps.test.ts b/__tests__/copyPasteProps.test.ts index 218fb6b..813fa15 100644 --- a/__tests__/copyPasteProps.test.ts +++ b/__tests__/copyPasteProps.test.ts @@ -15,7 +15,7 @@ describe('copyPasteNode', () => { strokes: 0, backgrounds: [] } - expect(copyPasteProps({ source, target })).toEqual({ + expect(copyPasteProps(source, target)).toEqual({ fills: 0, fillStyleId: 'soaowlqla', strokes: 1, @@ -36,7 +36,7 @@ describe('copyPasteNode', () => { strokes: 0, backgrounds: [] } - expect(copyPasteProps({ source, target, exclude: ['strokes'] })).toEqual({ + expect(copyPasteProps(source, target, { exclude: ['strokes'] })).toEqual({ fills: 0, fillStyleId: 'soaowlqla', strokes: 0, @@ -57,7 +57,7 @@ describe('copyPasteNode', () => { strokes: 0, backgrounds: [] } - expect(copyPasteProps({ source, target, include: ['strokes'] })).toEqual({ + expect(copyPasteProps(source, target, { include: ['strokes'] })).toEqual({ fills: 0, fillStyleId: '', strokes: 1, diff --git a/src/helpers/copyPasteProps.ts b/src/helpers/copyPasteProps.ts index d19bd1f..d106b31 100644 --- a/src/helpers/copyPasteProps.ts +++ b/src/helpers/copyPasteProps.ts @@ -17,13 +17,6 @@ * @param exclude - Props that shouldn't be copied */ -interface Arguments { - source: {} - target?: {} - include?: string[] - exclude?: string[] -} - const nodeProps: string[] = [ 'id', 'parent', @@ -112,7 +105,7 @@ const readonly: string[] = [ 'type' ] -export default function copyPasteProps({ source, target, include, exclude }: Arguments) { +export default function copyPasteProps(source, target?, { include, exclude }: any = {}) { let allowlist: string[] = nodeProps if (include) { @@ -147,19 +140,12 @@ export default function copyPasteProps({ source, target, include, exclude }: Arg for (const key2 in target) { if (allowlist.includes(key2)) { if (key1 === key2) { - o[key1] = copyPasteProps({ - source: val[key1], - target, - include, - exclude - }) + o[key1] = copyPasteProps(val[key1]) } } } } else { - if (allowlist.includes(key1)) { - o[key1] = copyPasteProps({ source: val[key1], include, exclude }) - } + o[key1] = copyPasteProps(val[key1]) } } @@ -167,9 +153,11 @@ export default function copyPasteProps({ source, target, include, exclude }: Arg !o.fillStyleId && o.fills ? null : delete o.fills !o.strokeStyleId && o.strokes ? null : delete o.strokes !o.backgroundStyleId && o.backgrounds ? null : delete o.backgrounds - } - return target ? Object.assign(target, o) : o + return target ? Object.assign(target, o) : o + } else { + return o + } } }