├── .DS_Store ├── Example.sketch ├── Images ├── Screen Shot 2019-01-28 at 2.59.21 PM.png ├── Screen Shot 2019-01-28 at 2.59.39 PM.png ├── Screen Shot 2019-01-28 at 3.00.03 PM.png ├── Screen Shot 2019-01-28 at 3.00.10 PM.png ├── Screen Shot 2019-01-28 at 3.01.17 PM.png ├── Sketch Arboards1.png ├── Sketch Artboards.png ├── Sketch Options.png └── Sketch Options1.png ├── README.md └── Table of Contents.sketchplugin └── Contents ├── Resources └── World.pdf └── Sketch ├── library.cocoascript ├── manifest.json └── script.cocoascript /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/.DS_Store -------------------------------------------------------------------------------- /Example.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Example.sketch -------------------------------------------------------------------------------- /Images/Screen Shot 2019-01-28 at 2.59.21 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Screen Shot 2019-01-28 at 2.59.21 PM.png -------------------------------------------------------------------------------- /Images/Screen Shot 2019-01-28 at 2.59.39 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Screen Shot 2019-01-28 at 2.59.39 PM.png -------------------------------------------------------------------------------- /Images/Screen Shot 2019-01-28 at 3.00.03 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Screen Shot 2019-01-28 at 3.00.03 PM.png -------------------------------------------------------------------------------- /Images/Screen Shot 2019-01-28 at 3.00.10 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Screen Shot 2019-01-28 at 3.00.10 PM.png -------------------------------------------------------------------------------- /Images/Screen Shot 2019-01-28 at 3.01.17 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Screen Shot 2019-01-28 at 3.01.17 PM.png -------------------------------------------------------------------------------- /Images/Sketch Arboards1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Sketch Arboards1.png -------------------------------------------------------------------------------- /Images/Sketch Artboards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Sketch Artboards.png -------------------------------------------------------------------------------- /Images/Sketch Options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Sketch Options.png -------------------------------------------------------------------------------- /Images/Sketch Options1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Images/Sketch Options1.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table-of-Contents 2 | **Updates (1/28)**: Made the plugin compatible with Sketch 53 beta. 3 | 4 | This is a Sketch Plugin I created upon a request from our Mozilla UX team. As a memember of the UX team we usually have to create different specs using Sketch, and we have to manually add a "Table of Contents" page to list out the contents. For this plugin you can just press the shortcut and it will automatically generate a Table of Contents page for you. (There are some rules to follow when you create your spec.) 5 | 6 | ![Create ToC Menu](https://github.com/youwenliang/Table-of-Contents/blob/master/Images/Screen%20Shot%202019-01-28%20at%202.59.21%20PM.png) 7 | ![ToC Artboard](https://github.com/youwenliang/Table-of-Contents/blob/master/Images/Screen%20Shot%202019-01-28%20at%202.59.39%20PM.png) 8 | 9 | ## Artboads Setup 10 | 1. Artboard size: larger than 1440 x 1024 preferred. 11 | 2. Artboard order: top first (Export as PDF order). 12 | 3. Having a Cover as the first artboard & Release Notes or Version History as the second (Support multiple version history pages now). 13 | 14 | 15 | ## Titles Setup 16 | 1. Use **Folders** named "Header1" & "Header2" as first level titles & second level titles. 17 | 18 | ![Titles Setup](https://github.com/youwenliang/Table-of-Contents/blob/master/Images/Screen%20Shot%202019-01-28%20at%203.01.17%20PM.png) 19 | 20 | ## Example File structure 21 | 1. Cover 22 | 2. Release Notes 23 | 3. Topic A (using Header1) 24 | 4. Topic A-1 (using Header2) 25 | 5. Topic A-2 (using Header2) 26 | 6. Topic A-3 (using Header2) 27 | 7. Topic B (using Header1) 28 | 8. Topic B-1 (using Header2) 29 | 9. Topic B-2 (using Header2) 30 | 10. Topic C (using Header1) 31 | ... 32 | 33 | ![File Structure](https://github.com/youwenliang/Table-of-Contents/blob/master/Images/Screen%20Shot%202019-01-28%20at%203.00.03%20PM.png) 34 | 35 | Checkout the Example Sketch file for more information: https://github.com/youwenliang/Table-of-Contents/blob/master/Example.sketch 36 | Table of Contents will be generated after the Cover artboard. 37 | Shortcut: control + option + ⌘command + T 38 | 39 | ## Additional Options: 40 | You can change the artboard background color, or change the margin and font size according to the amount of your contents. 41 | 42 | ## Refresh / Delete your ToC: 43 | If you've already generated a Table of Contents, you can click the button (control + option + ⌘command + T) again to refresh or delete your current Table of Contents. 44 | 45 | ![Options](https://github.com/youwenliang/Table-of-Contents/blob/master/Images/Screen%20Shot%202019-01-28%20at%203.00.10%20PM.png) 46 | -------------------------------------------------------------------------------- /Table of Contents.sketchplugin/Contents/Resources/World.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youwenliang/Table-of-Contents/1b030f2dab91ee446b2877bcde640b110513be77/Table of Contents.sketchplugin/Contents/Resources/World.pdf -------------------------------------------------------------------------------- /Table of Contents.sketchplugin/Contents/Sketch/library.cocoascript: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {NSMutableArray} array 3 | * @param {Number} atIndex 4 | * @param {Number} toIndex 5 | * @returns {NSMutableArray} 6 | */ 7 | 8 | function moveObject(array, atIndex, toIndex) { 9 | if (atIndex != toIndex) { 10 | var object = [[[array objectAtIndex:atIndex] retain] autorelease] 11 | 12 | [array removeObjectAtIndex:atIndex] 13 | [array insertObject:object atIndex:toIndex] 14 | } 15 | 16 | return array 17 | } 18 | 19 | 20 | 21 | /** 22 | * Sets `isSelected` to `true` for all given layers 23 | * @param {NSArray.} layers 24 | */ 25 | 26 | function restoreSelection(layers) { 27 | var loop = [layers objectEnumerator] 28 | 29 | while (layer = [loop nextObject]) { 30 | [layer setIsSelected:true] 31 | } 32 | } 33 | 34 | 35 | 36 | /** 37 | * Makes `NSApp sendAction` for `layer` `times` times 38 | * @param {NSDictionary} context 39 | * @param {String} action 40 | * @param {MSShapeGroup} layer 41 | * @param {Number} times 42 | */ 43 | 44 | function sendActionTimes(context, action, object, times) { 45 | var doc = context.document 46 | 47 | [[doc currentPage] deselectAllLayers] 48 | [object setIsSelected:true] 49 | 50 | for (var i = 0; i < times; i++) { 51 | [NSApp sendAction:action to:nil from:doc] 52 | } 53 | 54 | restoreSelection(context.selection) 55 | } 56 | 57 | 58 | 59 | /** 60 | * Returns layers sorted by first number in name 61 | * @param {NSArray.} layers 62 | * @returns {NSArray.} 63 | */ 64 | 65 | function sortLayers(layers, isAscending) { 66 | var sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:'name' 67 | ascending:isAscending 68 | selector:'localizedStandardCompare:'] 69 | 70 | return [layers sortedArrayUsingDescriptors:[sortDescriptor]] 71 | } 72 | 73 | 74 | 75 | /** 76 | * @typedef {Object} StepsStruct 77 | * @property {MSShapeGroup} layer 78 | * @property {Number} steps 79 | */ 80 | 81 | /** 82 | * Returns the quantity of steps that needed to reorder `layers` 83 | * in according to order of `sortedLayers` 84 | * @param {NSArray.} selection 85 | * @param {NSArray.} sortedLayers 86 | * @returns {Array.} 87 | */ 88 | 89 | function getSteps(selection, sortedLayers) { 90 | var steps = [] 91 | var selectedLayers = [selection mutableCopy] 92 | 93 | for (var i = 0; i < [sortedLayers count]; i++) { 94 | var layer = [sortedLayers objectAtIndex:i] 95 | var index = [selectedLayers indexOfObject:layer] 96 | 97 | steps.push({ layer: layer, steps: index - i }) 98 | 99 | selectedLayers = moveObject(selectedLayers, index, i) 100 | } 101 | 102 | return steps 103 | } 104 | 105 | 106 | 107 | /** 108 | * @param {NSArray.} selection 109 | * @returns {String} 110 | */ 111 | 112 | function getObjectName(selection) { 113 | var object = [selection firstObject] 114 | 115 | switch ([object class]) { 116 | case [MSShapeGroup class]: 117 | return 'layers' 118 | case [MSArtboardGroup class]: 119 | return 'artboards' 120 | default: 121 | return 'objects' 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Table of Contents.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Mark Liang", 3 | "commands" : [ 4 | { 5 | "script" : "script.cocoascript", 6 | "handler" : "onRun", 7 | "shortcut" : "cmd ctrl option t", 8 | "name" : "Table of Contents", 9 | "identifier" : "tableofcontents" 10 | } 11 | ], 12 | "menu": { 13 | "isRoot": true, 14 | "items": [ 15 | "tableofcontents" 16 | ] 17 | }, 18 | "identifier" : "com.example.sketch.tableofcontents", 19 | "version" : "1.2", 20 | "description" : "Sketch Plugin for creating a Table of Contents artboard automatically.", 21 | "authorEmail" : "mliang@mozilla.com", 22 | "name" : "Table of Contents" 23 | } 24 | -------------------------------------------------------------------------------- /Table of Contents.sketchplugin/Contents/Sketch/script.cocoascript: -------------------------------------------------------------------------------- 1 | 2 | @import "library.cocoascript"; 3 | 4 | var onRun = function(context) { 5 | var sketch = context.api(); 6 | var document = sketch.selectedDocument; 7 | var doc = context.document; // the current document (MSDocument) 8 | var plugin = context.plugin; // the plugin (MSPluginBundle) 9 | var page = [doc currentPage]; // the current page (MSPage) 10 | var artboards = doc.currentPage().artboards(); 11 | var numberArtboard = artboards.length; 12 | var Text = require('sketch/dom').Text 13 | 14 | // Prompt Dialog 15 | if(numberArtboard > 2) { 16 | var t = artboards[numberArtboard-1]; 17 | if([t name] == "Table of Contents") { 18 | showCheckWindow(doc, plugin, page, artboards, numberArtboard, context); 19 | } 20 | else { 21 | addTableOfContents(doc, plugin, page, artboards, numberArtboard, context); 22 | } 23 | } 24 | 25 | function showCheckWindow(doc, plugin, page, artboards, numberArtboard, context) { 26 | var alertWindow = COSAlertWindow.new() 27 | alertWindow.setMessageText('Do you want to refresh or delete your Table of Contents?') 28 | 29 | alertWindow.addButtonWithTitle('Refresh') 30 | alertWindow.addButtonWithTitle('Delete') 31 | 32 | 33 | if (alertWindow.runModal() == -NSModalResponseStop) { 34 | deleteTableOfContents(artboards); 35 | var a = doc.currentPage().artboards(); 36 | var n = a.length; 37 | addTableOfContents(doc, plugin, page, a, n, context); 38 | [doc showMessage: "Table of Contents refreshed."] 39 | 40 | } else { 41 | deleteTableOfContents(artboards); 42 | [doc showMessage: "Table of Contents deleted."] 43 | } 44 | } 45 | 46 | function showAlertWindow() { 47 | var alertWindow = COSAlertWindow.new() 48 | 49 | // Checkbox 50 | var regexpRename = regexpRename || {}; 51 | 52 | regexpRename.createCheckbox = function ( label, value, flag ) { 53 | 54 | flag = ( flag == false ) ? NSOffState : NSOnState; 55 | 56 | var checkbox = NSButton.alloc().initWithFrame( NSMakeRect( 0, 0, 300, 18 ) ); 57 | checkbox.setButtonType( NSSwitchButton ); 58 | checkbox.setTitle( label ); 59 | checkbox.setTag( value ); 60 | checkbox.setState( flag ); 61 | 62 | return checkbox; 63 | } 64 | 65 | alertWindow.setMessageText('Create Table of Contents') 66 | 67 | alertWindow.addTextLabelWithValue('Enter background color') 68 | alertWindow.addTextFieldWithValue('#FFFFFF') 69 | 70 | alertWindow.addTextLabelWithValue('Enter text color') 71 | alertWindow.addTextFieldWithValue('#4a4a4a') 72 | 73 | alertWindow.addTextLabelWithValue('Enter margin between contents') 74 | alertWindow.addTextFieldWithValue('50') 75 | 76 | alertWindow.addTextLabelWithValue('Enter contents font size') 77 | alertWindow.addTextFieldWithValue('20') 78 | 79 | //alertWindow.addAccessoryView( regexpRename.createCheckbox( 'Add numbers to title', 'a', true ) ) 80 | //alertWindow.addAccessoryView( regexpRename.createCheckbox( 'Show page numbers', 's', true ) ) 81 | 82 | alertWindow.addButtonWithTitle('OK') 83 | alertWindow.addButtonWithTitle('Cancel') 84 | 85 | if (alertWindow.runModal() == -NSModalResponseStop) { 86 | [doc showMessage: "Table of Contents created."] 87 | return new UserInput( 88 | alertWindow.viewAtIndex(1).stringValue(), 89 | alertWindow.viewAtIndex(3).stringValue(), 90 | alertWindow.viewAtIndex(5).stringValue(), 91 | alertWindow.viewAtIndex(7).stringValue(), 92 | //alertWindow.viewAtIndex(8).state(), 93 | //alertWindow.viewAtIndex(9).state() 94 | ) 95 | } else { 96 | return null 97 | } 98 | } 99 | 100 | // Dialog Results 101 | function UserInput(bgColor, txtColor, margin, fsize) { 102 | this.bgColor = bgColor 103 | this.txtColor = txtColor 104 | this.margin = margin 105 | this.fsize = fsize 106 | //this.titleN = titleN 107 | //this.pageN = pageN 108 | } 109 | 110 | function deleteTableOfContents(a){ 111 | var n = a.length 112 | a[n-1].removeFromParent(); 113 | for(var i = 0; i < n-1; i++) { 114 | var temp = a[i]; 115 | var l = (temp.children().toString().match(/ -1) ; 296 | // else { 297 | // if(header == 2 && count != 0) { 298 | // var titleNumber = count + "." + k + " "; 299 | // if(userInput.titleN == 0) { 300 | // titleNumber = "-"; 301 | // } 302 | // var textLayer = MSTextLayer.new(); 303 | // textLayer.setName([temp name]); 304 | // textLayer.setStringValue(" " + titleNumber + title); 305 | // textLayer.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("Fira Sans", userInput.fsize)); 306 | // selected_artboard.addLayers([textLayer]); 307 | 308 | // k++ 309 | // } else { 310 | // count++; 311 | // var titleNumber = count + ". "; 312 | // if(userInput.titleN == 0) { 313 | // titleNumber = ""; 314 | // } 315 | 316 | // var textLayer = MSTextLayer.new(); 317 | // textLayer.setName([temp name]); 318 | // textLayer.setStringValue(titleNumber + title); 319 | // textLayer.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("Fira Sans", userInput.fsize)); 320 | // selected_artboard.addLayers([textLayer]); 321 | 322 | // k = 1; 323 | // } 324 | // if(userInput.pageN == 1) { 325 | 326 | // var numberLayer = MSTextLayer.new(); 327 | // numberLayer.setName(page+""); 328 | // numberLayer.setStringValue(page+""); 329 | // numberLayer.addAttribute_value(NSFontAttributeName, NSFont.fontWithName_size("Fira Sans", userInput.fsize)); 330 | // selected_artboard.addLayers([numberLayer]); 331 | // } 332 | 333 | // // set content position 334 | // textLayer.frame().setX(100 + 500*parseInt(add/15)); 335 | // textLayer.frame().setY(100+(add%15)*(24+parseInt(userInput.margin)) + 120); 336 | // textLayer.setTextColor(MSImmutableColor.colorWithSVGString("#FFF").newMutableCounterpart()); 337 | 338 | // if(userInput.pageN == 1) { 339 | // numberLayer.frame().setX(500 + 500*parseInt(add/15)); 340 | // numberLayer.frame().setY(100+(add%15)*(24+parseInt(userInput.margin)) + 120); 341 | // numberLayer.setTextColor(MSImmutableColor.colorWithSVGString("#FFF").newMutableCounterpart()); 342 | // } 343 | 344 | // add++; 345 | // } 346 | // } 347 | } 348 | 349 | [selected_artboard select:true byExpandingSelection:false]; 350 | [view zoomToSelection]; 351 | } 352 | } 353 | } 354 | --------------------------------------------------------------------------------