├── Readme.md ├── Reverse Layer Order.sketchplugin ├── Reverse Positions.sketchplugin └── Sort Layers.sketchplugin /Readme.md: -------------------------------------------------------------------------------- 1 | ### Sort Layers 2 | 3 | Sorts the selected layers or artboards. Note: This plugin will also reorder the layers in the layer list to match the visual order. For best results, group your layers before sorting. 4 | 5 | There are also plugins to reverse the order of the layers in the layer list and a plugin that reverses the position of the selected layers on the artboard. 6 | 7 | **Options:** `Text (A->Z)`, `Text (Z->A)`, `Layer Name (A->Z)`, `Layer Name (Z->A)`, `Top`, `Left`, `Random` 8 | 9 | **Shortcut:** `ctrl` + `⌘` + `⌥` + `S` 10 | 11 | #### Sorting Layers by text, visually 12 | ![Selection Animation](https://dl.dropboxusercontent.com/u/974773/_keepalive/Style%20Inventory/Sorting.gif) 13 | 14 | #### Sorting Layers in the layer list by position 15 | ![Selection Animation](https://dl.dropboxusercontent.com/u/974773/_keepalive/Style%20Inventory/Sorting%20Layers%202.gif) 16 | 17 | #### Reverse Layer Order 18 | This will reverse the layer order. 19 | 20 | #### Reverse Positions 21 | This will reverse the position of the layers on the canvas. 22 | -------------------------------------------------------------------------------- /Reverse Layer Order.sketchplugin: -------------------------------------------------------------------------------- 1 | // This plugin reverses the positions of selected layers. 2 | 3 | #import 'inventory.js' 4 | 5 | var _selection = null; 6 | var leftPositions = []; 7 | var topPositions = []; 8 | var layersMeta = []; 9 | 10 | // Run 11 | if (selection.count() > 1) { 12 | 13 | // remember the selection 14 | _selection = selection; 15 | 16 | // sort selected layers 17 | sortLayers(selection); 18 | } else if (selection.count() == 1 && selection[0].children().count() > 0){ 19 | 20 | // remember the selection 21 | _selection = selection; 22 | 23 | var group = _selection[0]; 24 | // sort selected group 25 | sortLayers(group.layers().array()); 26 | } else { 27 | [doc showMessage:"Cannot sort single layers."] 28 | } 29 | 30 | // Main 31 | inventory.layers.reverseLayerOrder(layersMeta); 32 | inventory.layers.select(_selection); 33 | 34 | 35 | // Sorting 36 | function sortLayers (layers) { 37 | 38 | // Loop through all selected layers 39 | 40 | for (var i = 0; i < layers.count(); i++) { 41 | var layer = layers[i]; 42 | 43 | layersMeta.push({ 44 | "name": layer.name(), 45 | "layer": layer 46 | }); 47 | 48 | } 49 | } 50 | 51 | // Sorts numbers. By default, sort would handle numbers as strings and thus not sort them as intended. 52 | function sortNumber(a,b) { 53 | return a - b; 54 | } -------------------------------------------------------------------------------- /Reverse Positions.sketchplugin: -------------------------------------------------------------------------------- 1 | // This plugin reverses the positions of selected layers. 2 | 3 | #import 'inventory.js' 4 | 5 | var _selection = null; 6 | var leftPositions = []; 7 | var topPositions = []; 8 | var layersMeta = []; 9 | 10 | // Run 11 | if (selection.count() > 1) { 12 | 13 | // remember the selection 14 | _selection = selection; 15 | 16 | // sort selected layers 17 | sortLayers(selection); 18 | } else if (selection.count() == 1 && selection[0].children().count() > 0){ 19 | 20 | // remember the selection 21 | _selection = selection; 22 | 23 | var group = _selection[0]; 24 | // sort selected group 25 | sortLayers(group.layers().array()); 26 | } else { 27 | [doc showMessage:"Cannot sort single layers."] 28 | } 29 | 30 | // Restore selection 31 | inventory.layers.select(_selection); 32 | 33 | function sortLayers (layers) { 34 | 35 | // Loop through all selected layers 36 | 37 | for (var i = 0; i < layers.count(); i++) { 38 | var layer = layers[i]; 39 | 40 | layersMeta.push({ 41 | "name": layer.name(), 42 | "layer": layer, 43 | "top": layer.frame().top(), 44 | "left": layer.frame().left() 45 | }); 46 | 47 | // Remember the position of each layer 48 | 49 | topPositions.push(parseInt(layer.frame().y())); 50 | leftPositions.push(parseInt(layer.frame().x())); 51 | 52 | } 53 | 54 | // sort positions first 55 | topPositions.sort(sortNumber); 56 | leftPositions.sort(sortNumber); 57 | 58 | // then reverse positions 59 | topPositions.reverse(); 60 | leftPositions.reverse(); 61 | 62 | // Finally, layout the sorted layers 63 | 64 | // Sort layers by top positions 65 | layersMeta.sort(sortTop); 66 | 67 | for (var i = 0; i < layersMeta.length; i++) { 68 | layersMeta[i].layer.frame().setY(topPositions[i]); 69 | } 70 | 71 | // Sort layers by left positions 72 | layersMeta.sort(sortLeft); 73 | 74 | for (var i = 0; i < layersMeta.length; i++) { 75 | layersMeta[i].layer.frame().setX(leftPositions[i]); 76 | } 77 | } 78 | 79 | 80 | // Sorts numbers. By default, sort would handle numbers as strings and thus not sort them as intended. 81 | function sortNumber(a, b) { 82 | return a - b; 83 | } 84 | 85 | // Sorts layers by their top position 86 | function sortTop(a, b) { 87 | return a.top - b.top; 88 | } 89 | 90 | 91 | // Sorts layers by their top position 92 | function sortLeft(a, b) { 93 | return a.left - b.left; 94 | } 95 | -------------------------------------------------------------------------------- /Sort Layers.sketchplugin: -------------------------------------------------------------------------------- 1 | // (ctrl option command s) 2 | 3 | /** 4 | * This plugin can sort layers by positions, layer name or text value 5 | * 6 | * Florian Schulz Copyright 2014, MIT License 7 | */ 8 | 9 | 10 | #import 'inventory.js' 11 | 12 | var artboards = false; 13 | var keepTop = false; 14 | var keepLeft = false; 15 | var maxExamples = 3; 16 | var numberOfTextLayersPerGroup = 0; 17 | var _selection; 18 | var sortableValues = []; 19 | var sortIndex = 0; 20 | var layersMeta = []; 21 | var leftPositions = []; 22 | var topPositions = []; 23 | 24 | 25 | function sortLayers (_selection) { 26 | 27 | // Loop through all selected layers 28 | 29 | for (var i = 0; i < _selection.count(); i++) { 30 | 31 | var layers = _selection.objectAtIndex(i).children(); 32 | var textLayers = []; 33 | var numTextLayers = 0; 34 | var strings = []; 35 | 36 | // Check if the selected layers is an artboard 37 | 38 | if (_selection[i].className() == "MSArtboardGroup") { 39 | artboards = true; 40 | var layer = _selection[i]; 41 | } else { 42 | 43 | // Loop through all child layers of the group 44 | 45 | for (var j = 0; j < layers.count(); j++) { 46 | var layer = layers.objectAtIndex(j); 47 | 48 | // Proceed with text layers 49 | 50 | if (layer.className() == "MSTextLayer") { 51 | 52 | // Remember the string 53 | 54 | strings.push(layer.stringValue()); 55 | numTextLayers++; 56 | 57 | // Remember the maximum number of text layers per group 58 | 59 | if (numTextLayers > numberOfTextLayersPerGroup) { 60 | numberOfTextLayersPerGroup = numTextLayers; 61 | 62 | // Create example string for the drop down 63 | 64 | sortableString = layer.name(); 65 | sortableValues.push(sortableString); 66 | } else { 67 | if (i < maxExamples) { 68 | sortableValues[numTextLayers-1] += ", " + layer.name(); 69 | } else if(i == maxExamples) { 70 | sortableValues[numTextLayers-1] += ", …"; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | // For each layer group, save the corresponding values of the text layers 78 | 79 | layersMeta.push({ 80 | "name": layer.name(), 81 | "layer": _selection[i], 82 | "strings": strings, 83 | "top": _selection[i].frame().top(), 84 | "left": _selection[i].frame().left() 85 | }); 86 | 87 | // Remember the position of each layer 88 | 89 | topPositions.push(parseInt(_selection[i].frame().y())); 90 | leftPositions.push(parseInt(_selection[i].frame().x())); 91 | 92 | 93 | } 94 | 95 | // Sort positions 96 | 97 | topPositions.sort(sortNumber); 98 | leftPositions.sort(sortNumber); 99 | 100 | 101 | // Let the user select the text field that should be sorted on 102 | 103 | if (numberOfTextLayersPerGroup > 0) { 104 | 105 | // Show sort options 106 | 107 | var options = ["Text (A->Z)", "Text (Z->A)", "Layer Name (A->Z)", "Layer Name (Z->A)", "Top", "Left", "Random"]; 108 | var choice = createSelect('How do you want to sort?', options, 0) 109 | var index = choice[1]; 110 | sortMode = index; 111 | 112 | if (numberOfTextLayersPerGroup > 1 && sortMode < 2) { 113 | // Show text layer selection menu 114 | 115 | var choice = createSelect('What text layer do you want to sort on?', sortableValues, 0) 116 | var index = choice[1]; 117 | sortIndex = index; 118 | } 119 | 120 | switch (sortMode) { 121 | case 0: 122 | layersMeta.sort(sortText); 123 | break; 124 | case 1: 125 | layersMeta.sort(sortText); 126 | layersMeta.reverse(); 127 | break; 128 | case 2: 129 | layersMeta.sort(sortName); 130 | layersMeta.reverse(); 131 | break; 132 | case 3: 133 | layersMeta.sort(sortName); 134 | break; 135 | case 4: 136 | layersMeta.sort(sortTop); 137 | keepLeft = true; 138 | break; 139 | case 5: 140 | layersMeta.sort(sortLeft); 141 | keepTop = true; 142 | break; 143 | case 6: 144 | shuffle(layersMeta); 145 | break; 146 | default: 147 | break; 148 | } 149 | 150 | } else if (numberOfTextLayersPerGroup == 0) { 151 | 152 | // Limit the sort options if there are no text layers 153 | 154 | var options = ["Top", "Left", "Layer Name", "Layer Name (Reverse)"]; 155 | var choice = createSelect('How do you want to sort?', options, 0) 156 | var index = choice[1]; 157 | sortMode = index; 158 | 159 | switch (sortMode) { 160 | case 0: 161 | layersMeta.sort(sortTop); 162 | keepLeft = true; 163 | break; 164 | case 1: 165 | layersMeta.sort(sortLeft); 166 | keepTop = true; 167 | break; 168 | case 2: 169 | layersMeta.sort(sortName); 170 | break; 171 | case 3: 172 | layersMeta.sort(sortName); 173 | layersMeta.reverse(); 174 | break; 175 | default: 176 | break; 177 | } 178 | } 179 | 180 | // Finally, layout the sorted layers 181 | 182 | for (var i = 0; i < layersMeta.length; i++) { 183 | if (!keepTop) layersMeta[i].layer.frame().setY(topPositions[i]); 184 | if (!keepLeft) layersMeta[i].layer.frame().setX(leftPositions[i]); 185 | } 186 | } 187 | 188 | // Run 189 | if (selection.count() > 1) { 190 | 191 | // remember the selection 192 | _selection = selection; 193 | 194 | // sort selected layers 195 | sortLayers(selection); 196 | } else if (selection.count() == 1 && selection[0].children().count() > 0){ 197 | 198 | // remember the selection 199 | _selection = selection; 200 | 201 | var group = _selection[0]; 202 | 203 | // sort selected group 204 | sortLayers(group.layers().array()); 205 | } else { 206 | [doc showMessage:"Cannot sort single layers."] 207 | } 208 | 209 | var layersMetaArray = []; 210 | 211 | for (var i = 0; i < layersMeta.length; i++) { 212 | layersMetaArray.push(layersMeta[i].layer); 213 | } 214 | 215 | inventory.layers.sortIndices(layersMetaArray); 216 | 217 | // Restore selection 218 | inventory.layers.select(_selection); 219 | 220 | 221 | // Sorts text layer strings in a ascending order 222 | 223 | function asc(a, b) { 224 | if (a.strings[sortIndex] < b.strings[sortIndex]) 225 | return -1; 226 | if (a.strings[sortIndex] > b.strings[sortIndex]) 227 | return 1; 228 | return 0; 229 | } 230 | 231 | // Sorts text layer strings in a descending order 232 | 233 | function desc(a, b) { 234 | if (a.strings[sortIndex] > b.strings[sortIndex]) 235 | return -1; 236 | if (a.strings[sortIndex] < b.strings[sortIndex]) 237 | return 1; 238 | return 0; 239 | } 240 | 241 | // Sorts text layer strings in a ascending order 242 | 243 | function nameAsc(a, b) { 244 | if (a.name < b.name) 245 | return -1; 246 | if (a.name > b.name) 247 | return 1; 248 | return 0; 249 | } 250 | 251 | // Sorts text layer strings in a descending order 252 | function nameDesc(a, b) { 253 | if (a.name > b.name) 254 | return -1; 255 | if (a.name < b.name) 256 | return 1; 257 | return 0; 258 | } 259 | 260 | // Sorts numbers. By default, sort would handle numbers as strings and thus not sort them as intended. 261 | function sortNumber(a,b) { 262 | return a - b; 263 | } 264 | 265 | // Sorts layers by their top position 266 | function sortTop(a,b) { 267 | return a.top - b.top; 268 | } 269 | 270 | // Sorts layers by their top position 271 | function sortLeft(a, b) { 272 | return a.left - b.left; 273 | } 274 | 275 | 276 | // Shuffle array 277 | function shuffle(array) { 278 | var currentIndex = array.length, temporaryValue, randomIndex ; 279 | 280 | // While there remain elements to shuffle... 281 | while (0 !== currentIndex) { 282 | 283 | // Pick a remaining element... 284 | randomIndex = Math.floor(Math.random() * currentIndex); 285 | currentIndex -= 1; 286 | 287 | // And swap it with the current element. 288 | temporaryValue = array[currentIndex]; 289 | array[currentIndex] = array[randomIndex]; 290 | array[randomIndex] = temporaryValue; 291 | } 292 | 293 | return array; 294 | } 295 | 296 | /* 297 | * Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license 298 | * Author: Jim Palmer (based on chunking idea from Dave Koelle) 299 | */ 300 | 301 | function sortText (_a, _b) { 302 | var a = _a.strings[sortIndex]; 303 | var b = _b.strings[sortIndex]; 304 | 305 | return naturalSort(a, b); 306 | } 307 | 308 | function sortName (_a, _b) { 309 | var a = _a.name; 310 | var b = _b.name; 311 | 312 | return naturalSort(a, b); 313 | } 314 | 315 | function naturalSort (a, b) { 316 | var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, 317 | sre = /(^[ ]*|[ ]*$)/g, 318 | dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, 319 | hre = /^0x[0-9a-f]+$/i, 320 | ore = /^0/, 321 | i = function(s) { return naturalSort.insensitive && (''+s).toLowerCase() || ''+s }, 322 | // convert all to strings strip whitespace 323 | x = i(a).replace(sre, '') || '', 324 | y = i(b).replace(sre, '') || '', 325 | // chunk/tokenize 326 | xN = x.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'), 327 | yN = y.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'), 328 | // numeric, hex or date detection 329 | xD = parseInt(x.match(hre)) || (xN.length != 1 && x.match(dre) && Date.parse(x)), 330 | yD = parseInt(y.match(hre)) || xD && y.match(dre) && Date.parse(y) || null, 331 | oFxNcL, oFyNcL; 332 | // first try and sort Hex codes or Dates 333 | if (yD) 334 | if ( xD < yD ) return -1; 335 | else if ( xD > yD ) return 1; 336 | // natural sorting through split numeric strings and default strings 337 | for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { 338 | // find floats not starting with '0', string or 0 if not defined (Clint Priest) 339 | oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc]) || xN[cLoc] || 0; 340 | oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc]) || yN[cLoc] || 0; 341 | // handle numeric vs string comparison - number < string - (Kyle Adams) 342 | if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { return (isNaN(oFxNcL)) ? 1 : -1; } 343 | // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' 344 | else if (typeof oFxNcL !== typeof oFyNcL) { 345 | oFxNcL += ''; 346 | oFyNcL += ''; 347 | } 348 | if (oFxNcL < oFyNcL) return -1; 349 | if (oFxNcL > oFyNcL) return 1; 350 | } 351 | return 0; 352 | } 353 | // Creates a dialog with a drop down 354 | 355 | function createSelect(msg, items, selectedItemIndex){ 356 | selectedItemIndex = selectedItemIndex || 0 357 | 358 | var accessory = [[NSComboBox alloc] initWithFrame:NSMakeRect(0,0,200,25)] 359 | [accessory addItemsWithObjectValues:items] 360 | [accessory selectItemAtIndex:selectedItemIndex] 361 | 362 | var alert = [[NSAlert alloc] init] 363 | [alert setMessageText:msg] 364 | [alert addButtonWithTitle:'OK'] 365 | [alert addButtonWithTitle:'Cancel'] 366 | [alert setAccessoryView:accessory] 367 | 368 | var responseCode = [alert runModal] 369 | var sel = [accessory indexOfSelectedItem] 370 | 371 | return [responseCode, sel] 372 | } 373 | 374 | // Calls menu commands 375 | 376 | function sendAction(commandToPerform) { 377 | try { 378 | [NSApp sendAction:commandToPerform to:nil from:doc] 379 | } catch(e) { 380 | my.log(e) 381 | } 382 | }; 383 | 384 | function sendBackward() { 385 | sendAction('moveBackward:'); 386 | } 387 | function sendForward() { 388 | sendAction('moveForward:'); 389 | } 390 | function sendBack() { 391 | sendAction('moveToBack:'); 392 | } 393 | 394 | var numberOfTextLayersPerGroup = null; 395 | var sortableValues = null; 396 | var sortIndex = null; 397 | var layersMeta = null; 398 | var leftPositions = null; 399 | var topPositions = null; --------------------------------------------------------------------------------