├── .gitignore ├── README.md └── Symbols ├── README.md └── Sync Symbol.sketchplugin /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sketch Plugins 2 | 3 | This is a collection of plugins I've written for Bohemian Coding's 4 | [Sketch](http://bohemiancoding.com/sketch/). 5 | 6 | ## 1. [Symbols](https://github.com/tisho/sketch-plugins/tree/master/Symbols) 7 | 8 | ## DEPRECATED: Symbols are now part of Sketch 3. 9 | 10 | Symbols lets you mimic basic Smart Objects / Symbols functionality by automatically syncing changes between layer groups named in a particular way. As an added bonus, you can mark certain text layers in your symbols as dynamic and have their styles replicated, but not their content. 11 | 12 | [Watch demo video (~2.5 min)](https://vimeo.com/83370438) 13 | 14 | [See installation and usage instuctions](https://github.com/tisho/sketch-plugins/tree/master/Symbols). 15 | -------------------------------------------------------------------------------- /Symbols/README.md: -------------------------------------------------------------------------------- 1 | # Sketch Symbols 2 | 3 | ## DEPRECATED: Symbols are now part of Sketch 3. 4 | 5 | Sketch Symbols is a plug-in for [Sketch](http://bohemiancoding.com/sketch/) that lets you mimic basic Smart Objects / Symbols functionality by automatically syncing changes between layer groups named in a particular way. As an added bonus, you can mark certain text layers in your symbols as dynamic and have their styles replicated, but not their content. 6 | 7 | ## Demo 8 | 9 | [![Demo Video](http://tisho.github.io/sketch-plugins/images/demo-video-thumb.png)](https://vimeo.com/83370438) 10 | 11 | ## Installation 12 | 13 | 1. [Download the plugin.](https://github.com/tisho/sketch-plugins/archive/master.zip) 14 | 2. Double-click the file `Sync Symbol.sketchplugin` inside `Symbols/`. Sketch should open 15 | automatically and tell you that a new plugin was installed. 16 | 17 | Installed Plugin Message 18 | 19 | You should see the **Sync Symbol** entry under the Plugins menu now. 20 | 21 | Plugin Menu 22 | 23 | ## Usage 24 | 25 | 1. Create a layer group for your symbol. (`Cmd + G`) 26 | 2. Add **": symbol-name"** to its name to mark it as a symbol. *E.g.: "signup 27 | button : button-default".* 28 | 29 | ![Symbol Name Example](http://tisho.github.io/sketch-plugins/images/symbol-name.png) 30 | 31 | 3. Copy the same symbol to other parts of your document. You 32 | can change the name before the colon to whatever you like. 33 | E.g.: "ok button : button-default". 34 | 4. Make changes to any of the copies of the symbol you've created. With the symbol or one of its layers still selected, press `Cmd + E` to sync these changes with other instances of the symbol. 35 | 36 | **Bonus Tip:** Open *Preferences* and under *Layers* uncheck *"Append 37 | 'Copy' after duplicated layers"*, so you don't need to tweak the symbol 38 | name after you duplicate it. 39 | 40 | ## Dynamic Text Layers 41 | 42 | Put a `$` in front of the name of any text layer inside a symbol to mark 43 | it as dynamic. When you sync changes between symbols, dynamic text layers will 44 | not be replaced. Their styles, including font size, family and line height will be updated, but their content will remain 45 | intact. This lets you define a single symbol for a button, for example, but use 46 | different copy for each instance of that button. 47 | 48 | ![Dynamic Layer Name Example](http://tisho.github.io/sketch-plugins/images/dynamic-layer-name.png) 49 | 50 | ## Changing the Default Keyboard Shortcut 51 | 52 | 1. Open Sketch's plugins folder. You can do that easily by choosing 53 | Custom Script from the Plugins menu, then click the gear icon and 54 | choose "Open Plugins Folder". 55 | 2. Open the file "Sync Symbols.sketchplugin" in your favorite text 56 | editor. 57 | 3. The shortcut is on the first line: 58 | 59 | ``` 60 | // Syncs all instances of a symbol tagged with ": symbol-name" (cmd e) 61 | ``` 62 | 63 | Change it to whatever you like (ctrl shift s, for example), and you 64 | should be good to go. The following modifiers are all valid: `control ctrl alt option cmd command shift`. 65 | 66 | ## Updating the Plugin 67 | 68 | Right now there's no automated way to update plugins. You'll have to 69 | replace the plugin files manually. 70 | 71 | 1. [Download the latest version of the plugin.](https://github.com/tisho/sketch-plugins/archive/master.zip) 72 | 2. Open Sketch's plugins folder. You can do that easily by choosing 73 | Custom Script from the Plugins menu, then click the gear icon and 74 | choose "Open Plugins Folder". 75 | 3. Replace the file `Sync Symbol.sketchplugin` with its new version from 76 | the archive you downloaded. 77 | 78 | You don't need to restart Sketch. It will pick up the changes 79 | automatically. 80 | 81 | ## Issues and Questions 82 | 83 | [File an issue on Github](https://github.com/tisho/sketch-plugins/issues), send a message to [@tisho](http://twitter.com/tisho) on Twitter, or email . 84 | 85 | ## Thanks 86 | 87 | Bohemian Coding for creating [Sketch](http://bohemiancoding.com/sketch/) in the first place and [@bomberstudios](http://twitter.com/bomberstudios) for the wonderful [sketch-commands bundle](https://github.com/bomberstudios/sketch-commands), which proved a wonderful source for learning and inspiration. 88 | 89 | ## License 90 | 91 | The MIT License (MIT) 92 | 93 | Copyright (c) 2013 Tisho Georgiev 94 | 95 | Permission is hereby granted, free of charge, to any person obtaining a copy 96 | of this software and associated documentation files (the "Software"), to deal 97 | in the Software without restriction, including without limitation the rights 98 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 99 | copies of the Software, and to permit persons to whom the Software is 100 | furnished to do so, subject to the following conditions: 101 | 102 | The above copyright notice and this permission notice shall be included in 103 | all copies or substantial portions of the Software. 104 | 105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 108 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 109 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 110 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 111 | THE SOFTWARE. 112 | -------------------------------------------------------------------------------- /Symbols/Sync Symbol.sketchplugin: -------------------------------------------------------------------------------- 1 | // Syncs all instances of a symbol tagged with ": symbol-name" (cmd e) 2 | // v0.2 3 | 4 | var tagPattern = /:\s*(.*)$/; 5 | 6 | function alert(msg, title) { 7 | title = title || "Whoops"; 8 | var app = [NSApplication sharedApplication]; 9 | [app displayDialog:msg withTitle:title]; 10 | } 11 | 12 | function getNearestTaggedLayerGroup(ref) { 13 | var klass = [ref class]; 14 | if(klass === MSArtboardGroup || klass === MSPage) { 15 | return null; 16 | } 17 | 18 | while(ref && ([ref class] !== MSLayerGroup || ([ref class] === MSLayerGroup && ![ref name].match(tagPattern)))) { 19 | ref = [ref parentGroup]; 20 | } 21 | 22 | return ref; 23 | } 24 | 25 | function toJSArray(arr) { 26 | var len = arr.length(), res = []; 27 | 28 | while(len--) { 29 | res.push(arr[len]); 30 | } 31 | return res; 32 | } 33 | 34 | function filterNSArray(arr, test) { 35 | var len = arr.length(), res = []; 36 | while(len--) { 37 | if(test(arr[len])) { 38 | res.push(arr[len]); 39 | } 40 | } 41 | return res; 42 | } 43 | 44 | function isGroup(layer) { 45 | var klass = [layer class]; 46 | return klass === MSLayerGroup || klass === MSArtboardGroup; 47 | } 48 | 49 | function getLayerGroupsByTag(parent, tag) { 50 | var all = [parent layers]; 51 | // sometimes layers returns an instance of JSCocoaController, I'm not sure why 52 | if([all class] === JSCocoaController) return []; 53 | 54 | var groups = filterNSArray(all, isGroup), 55 | tagged = [], 56 | notTagged = []; 57 | 58 | groups.forEach(function(group) { 59 | var name = [group name]; 60 | var groupTag = name.match(tagPattern); 61 | if(groupTag && groupTag[1] === tag) { 62 | tagged.push(group); 63 | } else { 64 | nested = getLayerGroupsByTag(group, tag); 65 | Array.prototype.push.apply(tagged, nested); 66 | } 67 | }); 68 | 69 | return tagged; 70 | } 71 | 72 | function capitalize(str) { 73 | return str.slice(0, 1).toUpperCase() + str.slice(1); 74 | } 75 | 76 | function syncProperties(src, dst, props) { 77 | for(var j=0, k=props.length; j < k; j++) { 78 | var getter = props[j]; 79 | var setter = 'set' + capitalize(getter); 80 | 81 | dst[setter](src[getter]()); 82 | } 83 | } 84 | 85 | function copyLayerStyle(src, dst) { 86 | var srcStyle = [src style], 87 | dstStyle = [dst style], 88 | srcContext = [srcStyle contextSettings], 89 | dstContext = [dstStyle contextSettings], 90 | collections = ['borders', 'fills', 'shadows', 'innerShadows'], 91 | props = { 'borders': ['position', 'thickness', 'fillType', 'gradient', 'isEnabled'], 92 | 'fills': ['fillType', 'gradient', 'patternImage', 'noiseIntensity', 'isEnabled', 'color'], 93 | 'shadows': ['offsetX', 'offsetY', 'blurRadius', 'spread', 'color', 'isEnabled'], 94 | 'innerShadows': ['offsetX', 'offsetY', 'blurRadius', 'spread', 'color', 'isEnabled'], 95 | 'textLayer': ['fontSize', 'fontPostscriptName', 'textColor', 'textAlignment', 'characterSpacing', 'lineSpacing'] 96 | }; 97 | 98 | // copy layer styles 99 | collections.forEach(function(collection) { 100 | var srcCol = srcStyle[collection](), 101 | dstCol = dstStyle[collection](), 102 | propSet = props[collection]; 103 | 104 | for(var i=dstCol.length()-1; i >= 0; i--) { 105 | dstCol.removeStylePartAtIndex(i); 106 | } 107 | 108 | for(var i=0, l=srcCol.length(); i < l; i++) { 109 | var style = srcCol[i]; 110 | dstCol.addNewStylePart(); 111 | var newStyle = dstCol[dstCol.length() - 1]; 112 | 113 | syncProperties(style, newStyle, propSet); 114 | } 115 | }) 116 | 117 | // copy context settings 118 | [dstContext setOpacity:[srcContext opacity]]; 119 | [dstContext setBlendMode:[srcContext blendMode]]; 120 | 121 | // text layer-specific properties (font size, line spacing, etc.) 122 | if([dst class] === MSTextLayer) { 123 | syncProperties(src, dst, props['textLayer']); 124 | } 125 | } 126 | 127 | function copyLayerPosition(src, dst) { 128 | var srcFrame = [src frame], 129 | dstFrame = [dst frame]; 130 | 131 | if([src class] === MSTextLayer) { 132 | var textBehaviour = [src textBehaviour], // 0 = flexible, 1 = fixed 133 | alignment = [src textAlignment]; // 0 = left, 1 = right, 2 = center, 3 = justified 134 | 135 | if(textBehaviour === 0) { // flexible text behaviour 136 | switch(alignment) { 137 | case 0: // left 138 | [dstFrame setX:[srcFrame x]]; 139 | [dstFrame setY:[srcFrame y]]; 140 | break; 141 | case 1: // right 142 | [dstFrame setMaxX:[srcFrame maxX]]; 143 | [dstFrame setMaxY:[srcFrame maxY]]; 144 | break; 145 | case 2: // center 146 | case 3: // justified 147 | [dstFrame setMidX:[srcFrame midX]]; 148 | [dstFrame setMidY:[srcFrame midY]]; 149 | break; 150 | } 151 | } else { // fixed text behaviour 152 | [dstFrame setX:[srcFrame x]]; 153 | [dstFrame setY:[srcFrame y]]; 154 | [dstFrame setWidth:[srcFrame width]]; 155 | [dstFrame setHeight:[srcFrame height]]; 156 | } 157 | 158 | [dst setTextBehaviour:textBehaviour]; 159 | } else { 160 | [dstFrame setX:[srcFrame x]]; 161 | [dstFrame setY:[srcFrame y]]; 162 | [dstFrame setWidth:[srcFrame width]]; 163 | [dstFrame setHeight:[srcFrame height]]; 164 | } 165 | } 166 | 167 | (function main() { 168 | 169 | // HACK: on a freshly started Sketch instance, 'selection' is null until you select an object 170 | if(!(selection && [selection length])) { 171 | alert("Make sure you've selected a symbol, or a layer that belongs to one before you try to sync."); 172 | return; 173 | } 174 | 175 | var layerGroup = getNearestTaggedLayerGroup(selection[0]); 176 | if(!layerGroup) { 177 | alert("Make sure you've selected a symbol, or a layer that belongs to one before you try to sync."); 178 | return; 179 | } 180 | 181 | var name = [layerGroup name]; 182 | var tag = name.match(tagPattern); 183 | 184 | var tag = tag[1], 185 | pages = [doc pages], 186 | groups = []; 187 | 188 | for(var i=0, l=pages.length(); i < l; i++) { 189 | groups = Array.prototype.concat.apply(groups, getLayerGroupsByTag(pages[i], tag)); 190 | } 191 | 192 | var layers = toJSArray([layerGroup layers]); 193 | 194 | groups.forEach(function(group, i) { 195 | if(group === layerGroup) return; 196 | 197 | var targetLayers = toJSArray([group layers]), 198 | protectedLayerNames = [], 199 | protectedLayers = []; 200 | 201 | for(var i=0,l=targetLayers.length; i < l; i++) { 202 | var layer = targetLayers[i], 203 | name = ''+[layer name]; 204 | 205 | if(name.slice(0, 1) === '$') { 206 | protectedLayerNames.push(name); 207 | protectedLayers.push(targetLayers[i]); 208 | } 209 | 210 | group.removeLayer(targetLayers[i]); 211 | } 212 | 213 | for(var i=layers.length - 1; i >= 0; i--) { 214 | var layer = layers[i], 215 | name = ''+[layer name]; 216 | 217 | if(protectedLayerNames.indexOf(name) !== -1) { 218 | var protected = protectedLayers.pop(); 219 | copyLayerStyle(layer, protected); 220 | copyLayerPosition(layer, protected); 221 | group.addLayer(protected); 222 | 223 | } else { 224 | var copy = [layer duplicate]; 225 | layerGroup.removeLayer(copy); 226 | group.addLayer(copy); 227 | } 228 | } 229 | 230 | group.resizeRoot(); 231 | }); 232 | })(); 233 | --------------------------------------------------------------------------------