diff --git a/models/Layer/Layer.js b/models/Layer/Layer.js index fd02f6c..5d687c5 100644 --- a/models/Layer/Layer.js +++ b/models/Layer/Layer.js @@ -99,6 +99,35 @@ class Layer { return this.layers; } + /** + * Get all child, grandchild, etc layers, and optionally filter them by a predicate function. + * @param {function} [predicate] - Filter function if you want to only return certain layers + * @returns {Layer[]} An array of all layers. Will return all direct children, grandchildren, etc. + */ + getAllLayers(predicate) { + function getRecursiveLayers(current, list) { + if (current === null || current === undefined) { + return list; + } + const childLayers = current.getLayers(); + if (childLayers !== undefined) { + childLayers.forEach(layer => { + list.push(layer); + getRecursiveLayers(layer, list); + }); + } + return list; + } + + const allLayers = getRecursiveLayers(this, []); + + if (predicate) { + return allLayers.filter(predicate); + } + + return allLayers; + } + getID() { return this.do_objectID; } diff --git a/models/Layer/Layer.test.js b/models/Layer/Layer.test.js index f9e3c6c..c684c37 100644 --- a/models/Layer/Layer.test.js +++ b/models/Layer/Layer.test.js @@ -11,12 +11,74 @@ * and limitations under the License. */ -const Layer = require('./index'); - -const json = {}; +const Group = require('../Group'); +const Text = require('../Text'); describe('Layer', () => { it('should work from raw JSON', () => { expect(true).toBeTruthy(); }); }); + +describe('getLayers', () => { + it('should get all children layers', () => { + const group = new Group({ + name: 'group', + }); + const text1 = new Text({ + name: 'text1', + }); + const text2 = new Text({ + name: 'text2', + }); + + group.addLayer(text1); + group.addLayer(text2); + + const layers = group.getLayers().map(l => l.name); + + expect(layers.sort()).toEqual(['text1', 'text2'].sort()); + }); + + it('should only get direct children layers', () => { + const outerGroup = new Group({ + name: 'outer group', + }); + + const innerGroup = new Group({ + name: 'inner group', + }); + const text = new Text({ + name: 'text', + }); + + innerGroup.addLayer(text); + outerGroup.addLayer(innerGroup); + + const layers = outerGroup.getLayers().map(l => l.name); + + expect(layers.sort()).toEqual(['inner group'].sort()); + }); +}); + +describe('getAllLayers', () => { + it('should get all children layers', () => { + const outerGroup = new Group({ + name: 'outer group', + }); + + const innerGroup = new Group({ + name: 'inner group', + }); + const text = new Text({ + name: 'text', + }); + + innerGroup.addLayer(text); + outerGroup.addLayer(innerGroup); + + const layers = outerGroup.getAllLayers().map(l => l.name); + + expect(layers.sort()).toEqual(['inner group', 'text'].sort()); + }); +}); diff --git a/models/Layer/index.d.ts b/models/Layer/index.d.ts index 209b385..894ee9b 100644 --- a/models/Layer/index.d.ts +++ b/models/Layer/index.d.ts @@ -34,6 +34,8 @@ declare class Layer { getLayers(predicate?: string | RegExp): Layer[]; + getAllLayers(predicate?: string | RegExp): Layer[]; + getID(): string; } diff --git a/models/SymbolMaster/SymbolMaster.js b/models/SymbolMaster/SymbolMaster.js index ff77519..7020948 100644 --- a/models/SymbolMaster/SymbolMaster.js +++ b/models/SymbolMaster/SymbolMaster.js @@ -72,17 +72,100 @@ class SymbolMaster extends Artboard { } addLayer(layer, canOverride) { - this.overrideProperties.push( - Object.assign(MSImmutableOverrideProperty, { + const getOverrideNames = (prop, overrideNames) => { + switch (prop._class) { + case 'symbolInstance': + overrideNames.push(`${prop.do_objectID}_symbolID`); + break; + case 'text': + overrideNames.push(`${prop.do_objectID}_stringValue`); + if (prop.sharedStyleID !== null && prop.sharedStyleID !== undefined) { + overrideNames.push(`${prop.do_objectID}_textStyle`); + } + break; + case 'rectangle': + case 'star': + case 'triangle': + case 'polygon': + case 'shapePath': + if (prop.sharedStyleID !== null && prop.sharedStyleID !== undefined) { + overrideNames.push(`${prop.do_objectID}_layerStyle`); + } + break; + case 'group': + prop.layers.forEach(l => { + getOverrideNames(l, overrideNames); + }); + break; + default: + break; + } + return overrideNames; + }; + + getOverrideNames(layer, []).forEach(name => { + this.overrideProperties.push({ + ...MSImmutableOverrideProperty, canOverride, - overrideName: `${layer.do_objectID}_stringValue`, - }) - ); + overrideName: name, + }); + }); + super.addLayer(layer); } - createInstance(args) { - const symbolInstance = new SymbolInstance({ ...args, symbolID: this.symbolID }); + /** + * Update existing SymbolInstance + * Nested Symbols are not currently supported + * @property {symbolInstance} SymbolInstance + * @property {Object} args - overrides + * @property {string} args[].name - name of the override being set + * @property {string|Object} [args[].value] - the value set, for Layer Styles pass in object or do_objectID + * @property {string|Object} [args[].style] - for textStyles only, pass in TextStyle object or do_objectID + */ + updateInstance(symbolInstance, args) { + args.forEach(arg => { + const overrideLayer = this.getAllLayers().find(l => l.name === arg.name); + + if (overrideLayer !== undefined) { + const overrideName = overrideLayer.do_objectID; + + symbolInstance.overrideValues + .filter(prop => prop.overrideName.split('_')[0] === overrideName) + .forEach(prop => { + if (prop.overrideName.includes('_stringValue')) { + prop.value = arg.value; + } + if (prop.overrideName.includes('_layerStyle')) { + prop.value = arg.value instanceof Object ? arg.value.do_objectID : arg.value; + } + if (arg.extStyle && prop.overrideName.includes('_textStyle')) { + prop.value = arg.style instanceof Object ? arg.style.do_objectID : arg.style.do_objectID; + } + }); + } + }); + } + + /** + * Creates a new SymbolInstance with overrides + * Nested Symbols are not currently supported + * @property {Object} [args] + * @property {Object[]} [args.overrideValues] - overrides + * @property {string} [args.overrideValues[].name] - name of the override being set + * @property {string|Object} [args.overrideValues[].value] - the value set, for Layer Styles pass in object or do_objectID + * @property {string|Object} [args.overrideValues[].style] - for textStyles only, pass in TextStyle object or do_objectID + * @returns {SymbolInstance} + */ + createInstance(args = {}) { + const symbolInstance = new SymbolInstance({ + ...args, + symbolID: this.symbolID, + frame: new Rect(this.frame || {}), + name: args.name || '', + style: new Style(this.style), + allowsOverrides: this.allowsOverrides, + }); symbolInstance.overrideValues = this.overrideProperties.map(prop => ({ _class: 'overrideValue', @@ -91,6 +174,10 @@ class SymbolMaster extends Artboard { value: '', })); + if (args && args.overrideValues) { + this.updateInstance(symbolInstance, args.overrideValues); + } + return symbolInstance; } } diff --git a/models/SymbolMaster/SymbolMaster.test.js b/models/SymbolMaster/SymbolMaster.test.js index 7f162e4..15db337 100644 --- a/models/SymbolMaster/SymbolMaster.test.js +++ b/models/SymbolMaster/SymbolMaster.test.js @@ -13,6 +13,11 @@ const SymbolMaster = require('./index'); const Text = require('../Text'); +const Rectangle = require('../Rectangle'); +const SharedStyle = require('../SharedStyle'); +const TextStyle = require('../TextStyle'); +const Group = require('../Group'); + const json = require('./__SymbolMaster.json'); describe('SymbolMaster', () => { @@ -27,7 +32,9 @@ describe('SymbolMaster', () => { }); expect(symbolMaster.name).toEqual('test'); }); +}); +describe('addLayer', () => { it('should add overrideProperties', () => { const symbolMaster = new SymbolMaster({ name: 'test', @@ -42,7 +49,103 @@ describe('SymbolMaster', () => { expect(symbolMaster.overrideProperties[0].overrideName).toEqual(`${layer.do_objectID}_stringValue`); }); - it('should be able to create symbol instances', () => { + it('should create an override for each child layer', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const group = new Group({ + name: 'group', + }); + + const text = new Text({ + string: 'text1', + }); + + const text2 = new Text({ + string: 'text2', + }); + + group.addLayer(text); + group.addLayer(text2); + symbolMaster.addLayer(group); + + expect(symbolMaster.overrideProperties[0].overrideName).toEqual(`${text.do_objectID}_stringValue`); + expect(symbolMaster.overrideProperties[1].overrideName).toEqual(`${text2.do_objectID}_stringValue`); + }); + + it('should add override for text string and textStyle', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const blue = new SharedStyle({ + textStyle: { + _class: 'textStyle', + encodedAttributes: { + MSAttributedStringColorAttribute: { + _class: 'color', + alpha: 1, + blue: 1, + green: 0, + red: 0, + }, + }, + }, + }); + + const text = new Text({ + string: 'text', + }); + + text.setSharedStyle(blue); + symbolMaster.addLayer(text); + + expect(symbolMaster.overrideProperties[0].overrideName).toEqual(`${text.do_objectID}_stringValue`); + expect(symbolMaster.overrideProperties[1].overrideName).toEqual(`${text.do_objectID}_textStyle`); + }); + + it('should add override for layerStyle', () => { + const white = new SharedStyle({ + name: 'white', + fills: [ + { + color: '#fff', + }, + ], + }); + + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const layer = new Rectangle({ + width: 200, + height: 100, + x: 0, + y: 0, + name: 'rectangle', + }); + + layer.setSharedStyle(white); + symbolMaster.addLayer(layer); + + expect(symbolMaster.overrideProperties[0].overrideName).toEqual(`${layer.do_objectID}_layerStyle`); + }); +}); + +describe('createInstance', () => { + it('should be able to create a symbol instance', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const symbolInstance = symbolMaster.createInstance(); + + expect(symbolInstance.symbolID).toEqual(symbolMaster.symbolID); + }); + + it('should be able to create a symbol instance with overrideValues', () => { const symbolMaster = new SymbolMaster({ name: 'test', }); @@ -52,15 +155,207 @@ describe('SymbolMaster', () => { name: 'text layer', }); + symbolMaster.addLayer(layer, true); + const symbolInstance = symbolMaster.createInstance(); + + expect(symbolInstance.symbolID).toEqual(symbolMaster.symbolID); + expect(symbolInstance.overrideValues[0].overrideName).toEqual(symbolMaster.overrideProperties[0].overrideName); + }); + + it('should be able to create a symbol instance with text overrides', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const layer = new Text({ + name: 'text', + }); + + const overrideText = 'override text'; + symbolMaster.addLayer(layer, true); const symbolInstance = symbolMaster.createInstance({ - frame: { - width: 100, - height: 100, - }, + overrideValues: [ + { + name: 'text', + value: overrideText, + }, + ], }); - expect(symbolInstance.symbolID).toEqual(symbolMaster.symbolID); + expect(symbolInstance.overrideValues[0].value).toEqual(overrideText); + }); + + it('should be able to create a symbol instance with layer style overrides', () => { + const beforeStyle = new SharedStyle({ + name: 'before', + fills: [ + { + color: '#fff', + }, + ], + }); + + const afterStyle = new SharedStyle({ + name: 'after', + fills: [ + { + color: '#000', + }, + ], + }); + + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const layer = new Rectangle({ + width: 200, + height: 100, + x: 0, + y: 0, + name: 'rectangle', + }); + + layer.setSharedStyle(beforeStyle); + symbolMaster.addLayer(layer, true); + + const symbolInstance = symbolMaster.createInstance({ + overrideValues: [ + { + name: 'rectangle', + value: afterStyle, + }, + ], + }); + + expect(symbolInstance.overrideValues[0].overrideName).toEqual(symbolMaster.overrideProperties[0].overrideName); + expect(symbolInstance.overrideValues[0].value).toEqual(afterStyle.do_objectID); + }); + + it('should be able to create a symbol instance with overrides within groups', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const group = new Group({ + name: 'group', + }); + + const text = new Text({ + name: 'text', + }); + + const overrideText = 'override text'; + + group.addLayer(text); + + symbolMaster.addLayer(group, true); + + const symbolInstance = symbolMaster.createInstance({ + overrideValues: [ + { + name: 'text', + value: overrideText, + }, + ], + }); + + expect(symbolInstance.overrideValues[0].overrideName).toEqual(symbolMaster.overrideProperties[0].overrideName); + expect(symbolInstance.overrideValues[0].value).toEqual(overrideText); + }); +}); + +describe('updateInstance', () => { + it('should be able to update text Symbol Instance override', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const layer = new Text({ + name: 'text', + string: 'before', + }); + + const overrideText = 'after'; + + symbolMaster.addLayer(layer, true); + const symbolInstance = symbolMaster.createInstance({ name: 'instance' }); + + symbolMaster.updateInstance(symbolInstance, [{ name: 'text', value: overrideText }]); + + expect(symbolInstance.overrideValues[0].value).toEqual(overrideText); + }); + + it('should be able to update Symbol Instance override with layer style', () => { + const beforeStyle = new SharedStyle({ + name: 'before', + fills: [ + { + color: '#fff', + }, + ], + }); + + const afterStyle = new SharedStyle({ + name: 'after', + fills: [ + { + color: '#000', + }, + ], + }); + + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const layer = new Rectangle({ + width: 200, + height: 100, + x: 0, + y: 0, + name: 'rectangle', + }); + + layer.setSharedStyle(beforeStyle); + symbolMaster.addLayer(layer, true); + + const symbolInstance = symbolMaster.createInstance({ + name: 'instance', + }); + + symbolMaster.updateInstance(symbolInstance, [{ name: 'rectangle', value: afterStyle }]); + + expect(symbolInstance.overrideValues[0].overrideName).toEqual(symbolMaster.overrideProperties[0].overrideName); + expect(symbolInstance.overrideValues[0].value).toEqual(afterStyle.do_objectID); + }); + + it('should be able to update Symbol Instance override within groups', () => { + const symbolMaster = new SymbolMaster({ + name: 'symbol', + }); + + const group = new Group({ + name: 'group', + }); + + const text = new Text({ + name: 'text', + }); + + const overrideText = 'override text'; + + group.addLayer(text); + + symbolMaster.addLayer(group, true); + + const symbolInstance = symbolMaster.createInstance({ + name: 'instance', + }); + symbolMaster.updateInstance(symbolInstance, [{ name: 'text', value: overrideText }]); + expect(symbolInstance.overrideValues[0].overrideName).toEqual(symbolMaster.overrideProperties[0].overrideName); + expect(symbolInstance.overrideValues[0].value).toEqual(overrideText); }); }); diff --git a/models/SymbolMaster/index.d.ts b/models/SymbolMaster/index.d.ts index eb2ca19..9c75e91 100644 --- a/models/SymbolMaster/index.d.ts +++ b/models/SymbolMaster/index.d.ts @@ -1,5 +1,6 @@ import Artboard from '../Artboard'; import Layer from '../Layer'; +import SymbolInstance from '../SymbolInstance'; declare class SymbolMaster extends Artboard { symbolID: string; @@ -9,6 +10,10 @@ declare class SymbolMaster extends Artboard { constructor(args?: any, json?: any); // addLayer(layer: Layer, canOverride: boolean): this; + + updateInstance(symbolInstance: SymbolInstance, name: string, args: Object): any; + + createInstance(args: Object): SymbolInstance; } export = SymbolMaster;