├── nested.gif ├── overview.gif ├── selected.gif ├── assets └── icon.png ├── .gitignore ├── src ├── util.js ├── manifest.json └── index.js ├── package.json ├── README.md └── .appcast.xml /nested.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyault/sketch-adjusttofit/HEAD/nested.gif -------------------------------------------------------------------------------- /overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyault/sketch-adjusttofit/HEAD/overview.gif -------------------------------------------------------------------------------- /selected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyault/sketch-adjusttofit/HEAD/selected.gif -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyault/sketch-adjusttofit/HEAD/assets/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | plugin.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | .npm 7 | npm-debug.log 8 | 9 | # mac 10 | .DS_Store 11 | 12 | # WebStorm 13 | .idea 14 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | var util = module.exports = {}; 2 | 3 | /** 4 | * Add an 's' to a string if necessary 5 | * @param [number] num - the number of objects 6 | * @param [string] string - the name of the object 7 | * @returns [string] A string in the format of ' /s' 8 | */ 9 | util.pluralize = function(num, string) { 10 | if(num > 1) string += 's'; 11 | 12 | return `${num} ${string}`; 13 | }; 14 | 15 | /** 16 | * Add commas to an array of strings 17 | * @param [string[]] arr - the strings to commify 18 | * @returns [string] a commified string 19 | */ 20 | util.commify = function(arr) { 21 | switch(arr.length) { 22 | case 0: 23 | return; 24 | 25 | case 1: 26 | return arr[0]; 27 | 28 | case 2: 29 | return `${arr[0]} and ${arr[1]}`; 30 | 31 | default: 32 | let last = arr.pop(); 33 | 34 | return arr.join(', ') + ` and ${last}`; 35 | } 36 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adjusttofit", 3 | "description": "A quick Sketch plugin to resize text layers, groups, and artboards to fit their content. Also supports nested resizing.", 4 | "author": "Andrew Ault", 5 | "version": "1.2.2", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/andyault/sketch-adjusttofit.git" 9 | }, 10 | "engines": { 11 | "sketch": ">=3.0" 12 | }, 13 | "skpm": { 14 | "name": "adjustToFit", 15 | "manifest": "src/manifest.json", 16 | "main": "adjusttofit.sketchplugin", 17 | "assets": [ 18 | "assets/**/*" 19 | ] 20 | }, 21 | "scripts": { 22 | "build": "skpm-build", 23 | "watch": "skpm-build --watch", 24 | "start": "skpm-build --watch --run", 25 | "postinstall": "npm run build && skpm-link" 26 | }, 27 | "devDependencies": { 28 | "@skpm/builder": "^0.5.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adjustToFit 2 | 3 | ![overview-gif](https://github.com/andyault/sketch-adjusttofit/blob/master/overview.gif) 4 | 5 | A quick Sketch plugin to resize text layers, groups, and artboards to fit their content. Also supports nested resizing. 6 | 7 | ## Installation 8 | Head over to the [releases page](https://github.com/andyault/sketch-adjusttofit/releases) and follow the instructions 9 | 10 | ## Usage 11 | 12 | `⌃ + ⇧ + F`: Resize selected layer(s) including invisible children 13 | 14 | `⌘ + ⇧ + F`: Resize selected layer(s): 15 | 16 | ![selected-gif](https://github.com/andyault/sketch-adjusttofit/blob/master/selected.gif) 17 | 18 | `⌃ + ⌥ + ⇧ + F`: Resize selected layer(s) with nesting including invisible children 19 | 20 | `⌘ + ⌥ + ⇧ + F`: Resize selected layer(s) with nesting: 21 | 22 | ![nested-gif](https://github.com/andyault/sketch-adjusttofit/blob/master/nested.gif) 23 | 24 | ## Changelog 25 | 26 | #### 1.2.0 27 | * Added new commands to resize layers without excluding invisible children 28 | 29 | #### 1.1.0 30 | * Reworked textbox resize logic to fix a bug with blank lines 31 | * Updated gifs 32 | 33 | #### 1.0.0 34 | Init :) 35 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Adjust to Fit", 3 | "description": "A quick Sketch plugin to resize text layers, groups, and artboards to fit their content. Also supports nested resizing.", 4 | "author": "Andrew Ault", 5 | "homepage": "https://github.com/andyault/sketch-adjusttofit#changelog", 6 | "version": "1.2.2", 7 | "identifier": "com.andyault.adjusttofit", 8 | "icon": "icon.png", 9 | 10 | "compatibleVersion": 3, 11 | "bundleVersion": 1, 12 | "commands": [ 13 | { 14 | "name": "Selected Layers", 15 | "identifier": "adjust-selected", 16 | "shortcut": "command shift f", 17 | "script": "./index.js", 18 | "handler": "adjustSelected" 19 | }, 20 | { 21 | "name": "Nested Layers", 22 | "identifier": "adjust-nested", 23 | "shortcut": "command alt shift f", 24 | "script": "./index.js", 25 | "handler": "adjustNested" 26 | }, 27 | { 28 | "name": "Selected Layers (include invisible)", 29 | "identifier": "adjust-selected-invisible", 30 | "shortcut": "ctrl shift f", 31 | "script": "./index.js", 32 | "handler": "adjustSelectedInvisible" 33 | }, 34 | { 35 | "name": "Nested Layers (include invisible)", 36 | "identifier": "adjust-nested-invisible", 37 | "shortcut": "ctrl alt shift f", 38 | "script": "./index.js", 39 | "handler": "adjustNestedInvisible" 40 | } 41 | ], 42 | "menu": { 43 | "title": "Adjust To Fit", 44 | "items": [ 45 | "adjust-selected", 46 | "adjust-nested", 47 | "adjust-selected-invisible", 48 | "adjust-nested-invisible" 49 | ] 50 | } 51 | } -------------------------------------------------------------------------------- /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | //deps 2 | const sketch = require('sketch'); 3 | const util = require('./util'); 4 | 5 | //global object 6 | var app = { 7 | useCustomFunction: false 8 | }; 9 | 10 | //public methods 11 | /** 12 | * call adjustToFit on the selected layers 13 | */ 14 | app.adjustSelected = function() { 15 | app.runPlugin(false, false); 16 | }; 17 | 18 | /** 19 | * call adjustToFit on the selected layers and their children 20 | */ 21 | app.adjustNested = function() { 22 | app.runPlugin(false, true); 23 | }; 24 | 25 | /** 26 | * call custom adjustToFit that includes invisible children on the selected layers 27 | */ 28 | app.adjustSelectedInvisible = function() { 29 | app.runPlugin(true, false); 30 | }; 31 | 32 | /** 33 | * call custom adjustToFit that includes invisible children on the selected layers and their children 34 | */ 35 | app.adjustNestedInvisible = function() { 36 | app.runPlugin(true, true); 37 | }; 38 | 39 | //private methods 40 | /** 41 | * Call adjustLayers and print results 42 | * @param [boolean] includeInvisible - whether to use custom adjustToFit function 43 | * @param [boolean] nested - whether to adjust children 44 | */ 45 | app.runPlugin = function(includeInvisible, nested) { 46 | let document = sketch.getSelectedDocument(); 47 | let layers = document.selectedLayers; 48 | 49 | app.useCustomFunction = includeInvisible; 50 | 51 | let results = app.adjustLayers(layers, nested); 52 | 53 | sketch.UI.message(app.resultsString(results)); 54 | }; 55 | 56 | /** 57 | * Recursive functions to call adjustToFit on the given layers and return the results 58 | * @param [SketchLayer[]] layers - the layers to adjust 59 | * @param [boolean] nested - Whether this function should be called recursively for layers with children 60 | * @returns [object] The results 61 | */ 62 | app.adjustLayers = function(layers, nested) { 63 | let results = {}; 64 | 65 | layers.forEach((layer) => { 66 | let layerResults = {}; 67 | 68 | if(nested && layer.layers) { 69 | let nestedResults = app.adjustLayers(layer.layers, nested); 70 | 71 | for(let cat in nestedResults) 72 | if(nestedResults.hasOwnProperty(cat)) 73 | layerResults = nestedResults; 74 | 75 | delete layerResults.failed; 76 | } 77 | 78 | //keep track of whether or not the height was changed 79 | let oldHeight = layer.sketchObject.frame().height(); 80 | 81 | let result = app.adjustToFit(layer); 82 | 83 | let newHeight = layer.sketchObject.frame().height(); 84 | 85 | //group results by layer type, 'unaffected', or 'failed' 86 | let cat = 'failed'; 87 | 88 | if(result) { 89 | if(newHeight !== oldHeight) 90 | cat = layer.type.toLowerCase() 91 | else 92 | cat = 'unaffected'; 93 | } 94 | 95 | //add selected layer to layerResults 96 | layerResults[cat] = (layerResults[cat] + 1) || 1; 97 | 98 | //add layerResults to results 99 | for(let cat in layerResults) 100 | if(layerResults.hasOwnProperty(cat)) 101 | results[cat] = (results[cat] + layerResults[cat]) || layerResults[cat]; 102 | }); 103 | 104 | return results; 105 | }; 106 | 107 | /** 108 | * Call adjustToFit on a layer and return the result 109 | * @param [SketchLayer] layer - the layer to adjust 110 | * @returns [boolean] whether or not the layer was adjusted 111 | */ 112 | app.adjustToFit = function(layer) { 113 | switch(layer.type) { 114 | case 'Group': 115 | case 'Artboard': 116 | case 'SymbolMaster': 117 | app.adjustLayerToFit(layer); 118 | break; 119 | 120 | case 'Text': 121 | app.adjustTextToFit(layer); 122 | break; 123 | 124 | default: 125 | return false; 126 | } 127 | 128 | return true; 129 | }; 130 | 131 | /** 132 | * Adjust a layer based on its contents 133 | * @param [SketchLayer] layer - the layer to adjust 134 | */ 135 | app.adjustLayerToFit = function(layer) { 136 | if(app.useCustomFunction) { 137 | //make our own adjustToFit function 138 | //create a new rectangle for the layer 139 | let newFrame = { x: Infinity, y: Infinity, width: 0, height: 0 }; 140 | 141 | layer.layers.forEach((child) => { 142 | newFrame.x = Math.min(newFrame.x, child.frame.x); 143 | newFrame.y = Math.min(newFrame.y, child.frame.y); 144 | newFrame.width = Math.max(newFrame.width, child.frame.x + child.frame.width); 145 | newFrame.height = Math.max(newFrame.height, child.frame.y + child.frame.height); 146 | }); 147 | 148 | //adjust everything to new coordinates 149 | newFrame.width -= newFrame.x; 150 | newFrame.height -= newFrame.y; 151 | 152 | layer.layers.forEach((child) => { 153 | child.frame.x -= newFrame.x; 154 | child.frame.y -= newFrame.y; 155 | }); 156 | 157 | //apply new rectangle 158 | layer.frame.offset(newFrame.x, newFrame.y) 159 | layer.frame.width = newFrame.width; 160 | layer.frame.height = newFrame.height; 161 | } else 162 | layer.adjustToFit(); 163 | }; 164 | 165 | /** 166 | * Adjust a textbot based on its contents 167 | * @param [SketchLayer] layer - the text layer to adjust 168 | */ 169 | app.adjustTextToFit = function(layer) { 170 | let firstFrag = layer.fragments[0]; 171 | let lastFrag = layer.fragments[layer.fragments.length - 1]; 172 | 173 | //adjust y first for middle/bottom aligned text 174 | let oldY = layer.frame.y; 175 | let newY = oldY + firstFrag.rect.y; 176 | 177 | //add up heights of all lines, set new height 178 | let oldHeight = layer.frame.height; 179 | 180 | //this doesn't work because there can be space between lines 181 | //(as in empty lines aren't fragments) 182 | //let newHeight = layer.fragments.reduce((sum, frag) => (sum + frag.rect.height), 0); 183 | 184 | //instead, just do lastFrag.y + lastFrag.height and subtract firstFrag.y for 185 | //non-top-aligned text 186 | let newHeight = lastFrag.rect.y + lastFrag.rect.height - firstFrag.rect.y; 187 | 188 | if(newHeight !== oldHeight) { 189 | layer.frame.y = newY; 190 | layer.frame.height = newHeight; 191 | } 192 | }; 193 | 194 | /** 195 | * Build a string from a results object returned from #adjustLayers 196 | * @param [object] results - The results object to use 197 | * @returns [string] the results string 198 | */ 199 | app.resultsString = function(results) { 200 | //this isn't ideal but it's easier than putting layer types in their own object 201 | let numFailed = results.failed; 202 | delete results.failed; 203 | 204 | let numUnaffected = results.unaffected; 205 | delete results.unaffected; 206 | 207 | //create an array of '# things' strings 208 | let plurals = []; 209 | 210 | for(let cat in results) { 211 | if(results.hasOwnProperty(cat)) { 212 | let num = results[cat]; 213 | 214 | //lazy 215 | if(cat === 'text') cat = 'text layer'; 216 | if(cat === 'symbolmaster') cat = 'symbol'; 217 | 218 | plurals.push(util.pluralize(num, cat)); 219 | } 220 | } 221 | 222 | //only show relevant parts, join with comma 223 | let ret = []; 224 | 225 | if(plurals.length > 0) ret.push(`${util.commify(plurals)} adjusted successfully`); 226 | if(numUnaffected > 0) ret.push(`${util.pluralize(numUnaffected, 'layer')} unaffected`); 227 | if(numFailed > 0) ret.push(`${numFailed} failed`); 228 | 229 | return ret.join(', '); 230 | }; 231 | 232 | //done 233 | module.exports.adjustSelected = app.adjustSelected; 234 | module.exports.adjustNested = app.adjustNested; 235 | module.exports.adjustSelectedInvisible = app.adjustSelectedInvisible; 236 | module.exports.adjustNestedInvisible = app.adjustNestedInvisible; 237 | --------------------------------------------------------------------------------