diff --git a/__tests__/copyPasteProps.test.ts b/__tests__/copyPasteProps.test.ts new file mode 100644 index 0000000..813fa15 --- /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..d106b31 --- /dev/null +++ b/src/helpers/copyPasteProps.ts @@ -0,0 +1,165 @@ +/** + * 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 + */ + +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 }: any = {}) { + 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(val[key1]) + } + } + } + } else { + o[key1] = copyPasteProps(val[key1]) + } + } + + 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 + } else { + return 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,