├── ExampleFile.sketch ├── Flex-Layout.sketchplugin └── Contents │ └── Sketch │ ├── Layout │ ├── layout.JSLayoutHelper.js │ ├── layout.cssLayout.js │ ├── layout.cssParser.js │ ├── layout.js │ ├── layout.prototypes.js │ ├── lib │ │ ├── CSSJSON │ │ │ ├── cssjson.js │ │ │ └── json2.js │ │ └── css-layout │ │ │ ├── Layout-browserified.js │ │ │ ├── Layout.js │ │ │ └── Layout2.js │ └── utils.js │ └── manifest.json └── README.md /ExampleFile.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrescak/Sketch-Flex-Layout/cbb6a12b34b144c702ef7993ccd1a803165e0531/ExampleFile.sketch -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/layout.JSLayoutHelper.js: -------------------------------------------------------------------------------- 1 | // strict javascript, to be loaded in a javascript context 2 | // and aware of the css-layout framework in lib/css-layout/ 3 | 4 | 5 | // all of the CSS properties that are numeric as per readme 6 | // in https://github.com/facebook/css-layout 7 | var numericProperties = [ 8 | "width", "height", // positive 9 | "minWidth", "minHeight", // positive 10 | "maxWidth", "maxHeight", // positive 11 | "left", "right", "top", "bottom", 12 | "margin", "marginLeft", "marginRight", "marginTop", "marginBottom", 13 | "padding", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", // positive 14 | "borderWidth", "borderLeftWidth", "borderRightWidth", "borderTopWidth", "borderBottomWidth", // positive 15 | "flex" //positive 16 | ]; 17 | 18 | var provideComputedLayout = function(styleTree){ 19 | var parsedTree = parseLayoutToJSON(styleTree); 20 | parsedTree = insertMeasures(parsedTree); 21 | // feed the style tree to the css-layout 22 | computeLayout.fillNodes(parsedTree); 23 | computeLayout.computeLayout(parsedTree); 24 | var computedStyles = computeLayout.extractNodes(parsedTree); 25 | return computedStyles; 26 | } 27 | 28 | // insert measure functions into the node tree based on collected measures 29 | var insertMeasures = function(rootNode){ 30 | if (rootNode.hasOwnProperty("computedHeight")) { 31 | if (!rootNode.hasOwnProperty("style")) { 32 | rootNode.style = {}; 33 | }; 34 | rootNode.style.measure = function(width){ 35 | if (rootNode.hasOwnProperty("computedWidth")) { 36 | width = rootNode.computedWidth; 37 | } 38 | return {width:width, height:rootNode.computedHeight}; 39 | }; 40 | }; 41 | 42 | //iterate over children 43 | if (rootNode.hasOwnProperty("children")) { 44 | var nodeChildren = rootNode["children"]; 45 | for (var i = 0; i < nodeChildren.length; i++) { 46 | var nodeChild = nodeChildren[i]; 47 | nodeChildren[i] = insertMeasures(nodeChild); 48 | } 49 | } 50 | return rootNode; 51 | } 52 | 53 | // takes a json in string with all of the values as strings 54 | // converts all the string value that match a subset of keys 55 | // to numeric values 56 | var parseLayoutToJSON = function(styleString){ 57 | return JSON.parse(styleString, numericReviver); 58 | } 59 | 60 | // turns a string into a number if it matches numberProperties keys 61 | var numericReviver = function(key, value){ 62 | var returnValue = value; 63 | if (numericProperties.indexOf(key) > -1) { 64 | returnValue = Number(value); 65 | } 66 | return returnValue ; 67 | } 68 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/layout.cssLayout.js: -------------------------------------------------------------------------------- 1 | 2 | // apply a given stylesheet to classes and return the layer style tree 3 | var layerTreeWithStyles = function(stylesheet){ 4 | var layerTree = layerTreeWithStylesheet(stylesheet, page); 5 | return layerTree; 6 | } 7 | 8 | // collect all the measures from layers 9 | var collectMeasures = function(styleTree, computedStyleTree){ 10 | var measuredStyleTree = collectMeasuresRecursively(page, styleTree, computedStyleTree, false); 11 | return measuredStyleTree; 12 | } 13 | 14 | // compute layout given the metadata style tree 15 | var computeStyles = function(styleTree){ 16 | //load css-layout 17 | var jsContext = [[JSContext alloc] init]; 18 | var layoutLib = utils.js.loadLibrary("lib/css-layout/Layout.js"); 19 | [jsContext evaluateScript:layoutLib]; 20 | var layoutHelper = utils.js.loadLibrary("layout.JSLayoutHelper.js"); 21 | [jsContext evaluateScript:layoutHelper]; 22 | var computeLayout = jsContext[@"provideComputedLayout"]; 23 | 24 | //stringify the layer tree 25 | var stringifiedStyles = stringifyStyleTree(styleTree); 26 | 27 | //compute styles 28 | var layoutArguments = NSArray.arrayWithObjects(stringifiedStyles); 29 | var computedStyles = [computeLayout callWithArguments:layoutArguments]; 30 | 31 | //in case something goes to hell - todo - throw an alert if not empty 32 | if ([jsContext exception]) { 33 | utils.UI.showError("CSS Layout: " + [jsContext exception]); 34 | }; 35 | return [computedStyles toDictionary]; 36 | } 37 | 38 | // lay out all the layers given computed styles 39 | var layoutElements = function(styleTree, computedTree){ 40 | layoutLayersRecursively(styleTree, computedTree, 0,0, page, false); 41 | } 42 | 43 | // traverse all of the layers and lay out the elements 44 | var layoutLayersRecursively = function(styleTree, computedTree, currentX, currentY, currentLayer, layoutChildrenFlag) 45 | { 46 | var shouldLayoutChildren = layoutChildrenFlag; 47 | // check if the item has a style attribute and if yes, turn the flag on 48 | // to lay out itself and all of it's children. This is to only start laying out 49 | // from the topmost item in the hierarchy that has a style attribute. 50 | if (styleTree.hasOwnProperty("style")) { 51 | shouldLayoutChildren = true; 52 | } 53 | 54 | // don't lay out children of layers that start with "@" 55 | if (shouldIgnoreLayer(currentLayer)) { 56 | shouldLayoutChildren = false; 57 | } 58 | //ignore top page and stylesheet layer 59 | //todo - handle artboards better 60 | if (shouldLayoutChildren && !shouldIgnoreLayer(currentLayer)) { 61 | 62 | // 0 gets undefined when it's passed as a parameter *gosh* 63 | if (!currentY) { currentY = 0; }; 64 | if (!currentX) { currentX = 0; }; 65 | 66 | var relativeY = currentY + computedTree["top"]; 67 | var relativeX = currentX + computedTree["left"]; 68 | 69 | var positionRect = currentLayer.rect(); 70 | positionRect.origin.x = relativeX; 71 | positionRect.origin.y = relativeY; 72 | currentLayer.setRect(positionRect); 73 | 74 | // don't set size on groups, it resizes based on children and would fuck things up 75 | if (!utils.is.group(currentLayer)) { 76 | var sizeRect = [currentLayer rect]; 77 | sizeRect.size.width = computedTree["width"]; 78 | sizeRect.size.height = computedTree["height"]; 79 | [currentLayer setRect:sizeRect]; 80 | } 81 | } 82 | 83 | // iterate over children recursively if we can 84 | if (utils.is.group(currentLayer)){ 85 | var childLayers = [currentLayer layers]; 86 | var childComputedTree = computedTree["children"]; 87 | var childStyleTree = styleTree["children"]; 88 | var parentX = currentLayer.frame.x; 89 | var parentY = currentLayer.frame.y; 90 | if (childLayers){ 91 | for (var i=0; i < [childLayers count]; i++){ 92 | var childLayer = childLayers[i]; 93 | 94 | // special case for group background to stretch to parent group size 95 | if (childLayer.name() == backgroundLayerName) { 96 | var childRect = [childLayer rect]; 97 | childRect.origin.y = 0; 98 | childRect.origin.x = 0; 99 | childRect.size.width = computedTree["width"]; 100 | childRect.size.height = computedTree["height"]; 101 | [childLayer setRect:childRect]; 102 | } 103 | layoutLayersRecursively(childStyleTree[i], childComputedTree[i], parentX, parentY, childLayer, shouldLayoutChildren); 104 | } 105 | // make sure group's bounds are re-set 106 | [currentLayer resizeToFitChildrenWithOption:1]; 107 | } 108 | } 109 | } 110 | 111 | // recursively iterate through all layers, find text layers within elements that 112 | // should be laid out, measure their height based on the computed tree 113 | // and add it back to the style tree 114 | var collectMeasuresRecursively = function(currentLayer, styleTree, computedStyleTree, shouldCollectMeasures){ 115 | // only collect measures if it's a descendant of a styled element 116 | if (styleTree.hasOwnProperty("style")) { 117 | shouldCollectMeasures = true; 118 | } 119 | //collect measures if it's a text layer 120 | if (shouldCollectMeasures && utils.is.textLayer(currentLayer)) { 121 | // parent elements have width and text should behave appropriately 122 | if (computedStyleTree["width"] > 0) { 123 | [currentLayer setTextBehaviour:1] // BCTextBehaviourFixedWidth 124 | var currentRect = [currentLayer rect]; 125 | currentRect.size.width = computedStyleTree["width"]; 126 | [currentLayer setRect:currentRect]; 127 | [currentLayer adjustFrameToFit]; 128 | // parent elements have no width, they should behave according to the text layer 129 | } else { 130 | styleTree["computedWidth"] = currentLayer.rect().size.width; 131 | } 132 | 133 | styleTree["computedHeight"] = currentLayer.rect().size.height; 134 | } 135 | 136 | // iterate over children recursively if we can 137 | if (utils.is.group(currentLayer)){ 138 | var childLayers = [currentLayer layers]; 139 | var childStyleTree = styleTree["children"]; 140 | var childComputedTree = computedStyleTree["children"]; 141 | if (childLayers){ 142 | for (var i=0; i < [childLayers count]; i++){ 143 | var childLayer = childLayers[i]; 144 | styleTree["children"][i] = collectMeasuresRecursively(childLayer, childStyleTree[i], childComputedTree[i], shouldCollectMeasures); 145 | } 146 | } 147 | } 148 | return styleTree; 149 | } 150 | 151 | // ----------------- helpers ---------------- // 152 | 153 | // takes a stylesheet, returns a tree of layers with the individual selector 154 | // styles applied to layers that match the selector, also applies 155 | // absolute styles for backgrounds and style layers so they're not moved 156 | var layerTreeWithStylesheet = function(stylesheet, layer){ 157 | var layerInfo = {}; 158 | // ignore stylesheets 159 | var layerName = layer.name(); 160 | layerInfo["name"] = layerName; 161 | if (layerName == styleSheetLayerName || utils.is.page(layer)) { 162 | 163 | // ignore stylesheets and pages 164 | 165 | } else if ([layerName hasPrefix:"@"] || layerName == backgroundLayerName){ 166 | 167 | // add position absolute to style layers and backgrounds so their sizes are not computed 168 | layerStyle = {}; 169 | layerStyle["position"] = "absolute"; 170 | layerInfo["style"] = layerStyle; 171 | 172 | } else { 173 | 174 | // check if we should style this layer and add it to the style 175 | for (var selector in stylesheet) { 176 | if (utils.common.endsWithString(layerName, selector)) { 177 | var style = stylesheet[selector]; 178 | layerInfo["style"] = style.attributes; 179 | } 180 | } 181 | } 182 | 183 | // iterate over children recursively if we can 184 | if (utils.is.group(layer)){ 185 | var childLayers = [layer layers]; 186 | if (childLayers){ 187 | var childrenArray = []; 188 | var loop = [childLayers objectEnumerator]; 189 | while (item = [loop nextObject]) { 190 | var childLayerInfo = layerTreeWithStylesheet(stylesheet, item); 191 | //todo - in case stylesheet is somewhere deep we should remove this 192 | childrenArray.push(childLayerInfo); 193 | } 194 | layerInfo["children"] = childrenArray; 195 | } 196 | } 197 | return layerInfo; 198 | } 199 | 200 | // takes an obj-c dictionary of all layers, together with styles 201 | // returns string to pass along to layout helper 202 | var stringifyStyleTree = function(styleTree){ 203 | // first converst the tree to stringified JSON 204 | var JSONData = [NSJSONSerialization dataWithJSONObject:styleTree options:0 error:nil]; 205 | var JSONString = [[NSString alloc] initWithData:JSONData encoding:NSUTF8StringEncoding]; 206 | return JSONString; // JSValue to pass along to the css-layout library; 207 | } 208 | 209 | // check whether a layer should be ignored when being laid out 210 | var shouldIgnoreLayer = function(currentLayer){ 211 | if ([currentLayer class] == "MSPage") { 212 | return true; 213 | } 214 | if (currentLayer.name() == styleSheetLayerName) { 215 | return true; 216 | } 217 | if (currentLayer.name() == backgroundLayerName) { 218 | return true; 219 | } 220 | if ([[currentLayer name] hasPrefix:"@"]) { 221 | return true; 222 | } 223 | return false; 224 | } 225 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/layout.cssParser.js: -------------------------------------------------------------------------------- 1 | // returns parsed CSS retrieved from style sheet layer 2 | var parseStyleSheetLayer = function() { 3 | var parsedCSS = {} 4 | var styleSheetLayer = getStyleSheetLayer(styleSheetLayerName); 5 | if (styleSheetLayer) { 6 | var cssString = "" + [styleSheetLayer stringValue]; //hack to circumvent cocoa string; 7 | var parsedCSS = parseCss(cssString); 8 | } else { 9 | log("No stylesheet found, proceeding to prototypes"); 10 | } 11 | return (parsedCSS); 12 | } 13 | 14 | // get the styleSheet layer 15 | var getStyleSheetLayer = function(layerName){ 16 | var layer = nil; 17 | var pageLayers = [page layers]; 18 | if (pageLayers) { 19 | var loop = [pageLayers objectEnumerator]; 20 | while (item = [loop nextObject]) { 21 | if ([item class] == "MSTextLayer" && [item name] == styleSheetLayerName) 22 | layer = item; 23 | } 24 | } 25 | return layer; 26 | } 27 | 28 | // takes a CSS string and returns a json with all the rules 29 | var parseCss = function(cssToParse) { 30 | var jsContext = [[JSContext alloc] init]; 31 | var jsonLib = utils.js.loadLibrary("lib/CSSJSON/json2.js"); 32 | [jsContext evaluateScript:jsonLib]; 33 | var parser = utils.js.loadLibrary("lib/CSSJSON/cssjson.js"); 34 | [jsContext evaluateScript:parser]; 35 | var parseScript = jsContext[@"CSSJSON"][@"toJSON"]; 36 | var parseArguments = NSArray.arrayWithObjects(cssToParse); 37 | var parsedCSS = [parseScript callWithArguments:parseArguments]; 38 | return [parsedCSS toDictionary].children; 39 | } 40 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/layout.js: -------------------------------------------------------------------------------- 1 | // Layout 2 | // v0.5 3 | // ======================================= 4 | // Apply CSS Layout to Sketch elements 5 | // Matej Hrescak (matejh@fb.com) 6 | 7 | @import 'utils.js' 8 | @import 'layout.cssParser.js' 9 | @import 'layout.prototypes.js' 10 | @import 'layout.cssLayout.js' 11 | 12 | var styleSheetLayerName = "@stylesheet"; 13 | var backgroundLayerName = "bg"; 14 | 15 | var onRun = function(context) { 16 | utils.init(context); 17 | debug.start(); 18 | // parsing prototypes 19 | var parsedSheet = parseStyleSheetLayer(); 20 | var prototypeSheet = parsePrototypes(); 21 | for(var attr in prototypeSheet){ parsedSheet[attr] = prototypeSheet[attr]}; 22 | debug.logPart("Stylesheet + prototypes parsed"); 23 | //log(parsedSheet); 24 | 25 | // apply stylesheet to layers and return layer tree with styles 26 | var styleTree = layerTreeWithStyles(parsedSheet); 27 | debug.logPart("Applying stylesheet to layers") 28 | //log(styleTree) 29 | 30 | // compute layout 31 | var computedTree = computeStyles(styleTree); 32 | debug.logPart("Computed styles"); 33 | //log(computedTree); 34 | 35 | // measure text layers 36 | var measuredStyleTree = collectMeasures(styleTree, computedTree); 37 | debug.logPart("Measures collected"); 38 | //log(measuredStyleTree); 39 | 40 | // recompute the tree again 41 | computedTree = computeStyles(measuredStyleTree); 42 | debug.logPart("Recomputed styles"); 43 | //log(computedTree); 44 | 45 | // lay out the elements 46 | layoutElements(styleTree, computedTree); 47 | layoutPrototypes(); 48 | debug.logPart("Layer layout styles"); 49 | debug.end(); 50 | } 51 | 52 | var newObjectFromPrototype = function(context){ 53 | utils.init(context); 54 | var prototypeLayers = getPrototypeLayers(); 55 | var chosenPrototype; 56 | // let people choose from more prototypes or select the single one 57 | if (prototypeLayers.length == 0) { 58 | utils.UI.showMessage("There appear to be no prototypes in this document") 59 | return; 60 | } else if (prototypeLayers.length == 1) { 61 | chosenPrototype = prototypeLayers[0]; 62 | } else { 63 | var prototypeNames = utils.common.arrayOfValuesByKey(prototypeLayers,"name"); 64 | var dialogResult = utils.UI.showSelect("Choose a prototype",prototypeNames); 65 | if (dialogResult[0] == NSAlertFirstButtonReturn) { 66 | chosenIndex = dialogResult[1]; 67 | chosenPrototype = prototypeLayers[chosenIndex]; 68 | } 69 | } 70 | // duplicate and clean up the prototype instance 71 | var newLayer = instantiatePrototype(chosenPrototype.layer); 72 | // move it to selection if there is selection 73 | if([selection count] != 0){ 74 | utils.misc.moveLayerToSelection(newLayer); 75 | onRun(context); 76 | } else { 77 | // otherwise select the newly created layer 78 | [newLayer select:true byExpandingSelection:false]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/layout.prototypes.js: -------------------------------------------------------------------------------- 1 | var prototypeKeyword = "prototype"; 2 | var styleHandle = "@"; 3 | var styleLayerName = "styles"; 4 | var compoundProperties = ["margin", "padding", "size"]; 5 | var widthProperties = ["width", "minWidth", "maxWidth", "left", "right", "marginLeft", "marginRight", "paddingLeft", "paddingRight"]; 6 | var heightProperties = ["height", "minHeight", "maxHeight", "top", "bottom", "marginTop", "marginBottom", "paddingTop", "paddingBottom"]; 7 | 8 | // parse all the prototypes on the current page 9 | var parsePrototypes = function(){ 10 | // look for prototypes in page recursively 11 | var styleArray = parseLayersForPrototypes(page,false); 12 | var flattenedArray = utils.common.flattenArray(styleArray); 13 | log(flattenedArray.length + " classes found in prototypes"); 14 | var styleObject = utils.common.keyedObjectFromArray(flattenedArray, "class"); 15 | return styleObject; 16 | } 17 | 18 | // lays out prototype styles 19 | var layoutPrototypes = function(){ 20 | utils.call.pageLayers(function(layer){ 21 | if (isLayerAPrototype(layer)) { 22 | utils.call.childLayers(layer, function(currentLayer){ 23 | layoutPrototypeStyles(currentLayer); 24 | }) 25 | }; 26 | }); 27 | } 28 | 29 | // creates a new object from the given prototype 30 | var instantiatePrototype = function(layer){ 31 | // remove prototype from layer 32 | var newLayer = [layer duplicate]; 33 | [newLayer setName:getPrototypeClassFromLayer(newLayer)]; 34 | 35 | // rename size layers and collect style layers 36 | var layersToDelete = []; 37 | utils.call.childLayers(newLayer, function(currentLayer){ 38 | if (isLayerAStyleLayer(currentLayer)) { 39 | if (currentLayer.name() == styleHandle + "size") { 40 | [currentLayer setName:backgroundLayerName]; 41 | } else { 42 | layersToDelete.push(currentLayer); 43 | } 44 | } 45 | }); 46 | 47 | // remove style layers 48 | for (var i = 0; i < layersToDelete.length; i++) { 49 | var layerToDelete = layersToDelete[i]; 50 | [layerToDelete removeFromParent]; 51 | } 52 | return newLayer; 53 | } 54 | 55 | // recursively look for prototypes and collect styles in an array 56 | var parseLayersForPrototypes = function(baseLayer,shouldCollectStyles){ 57 | //if layer is a prototype, flip a switch to parse all of the children for styles 58 | var parsedStyles = []; 59 | if (isLayerAPrototype(baseLayer)) { 60 | shouldCollectStyles = true; 61 | } 62 | // you can only collect styles on groups 63 | if (utils.is.group(baseLayer)){ 64 | if (shouldCollectStyles) { 65 | var styleAttributes = collectAttributes(baseLayer); 66 | if (utils.common.objectSize(styleAttributes) > 0) { 67 | var styleObject = {}; 68 | styleObject["class"] = getClassFromLayer(baseLayer); 69 | styleObject["attributes"] = styleAttributes; 70 | parsedStyles.push(styleObject); 71 | } else { 72 | utils.UI.showMessage(baseLayer.name() + " has no styles attached"); 73 | } 74 | } 75 | var childLayers = [baseLayer layers]; 76 | if (childLayers){ 77 | for (var i=0; i < [childLayers count]; i++){ 78 | var item = childLayers[i]; 79 | var childStyles = parseLayersForPrototypes(item,shouldCollectStyles); 80 | parsedStyles.push(childStyles); 81 | } 82 | } 83 | } 84 | return parsedStyles; 85 | } 86 | 87 | // returns whether a layer group is a prototype or not 88 | var isLayerAPrototype = function(layer){ 89 | return [[layer name] hasPrefix:prototypeKeyword]; 90 | } 91 | 92 | // returns whether a layer is one of the style layers within a prototype 93 | var isLayerAStyleLayer = function(layer){ 94 | var layerName = layer.name(); 95 | var allStyleLayers = compoundProperties.concat(widthProperties).concat(heightProperties); 96 | allStyleLayers.push(styleLayerName); 97 | for (var i = 0; i < allStyleLayers.length; i++) { 98 | var styleName = styleHandle + allStyleLayers[i]; 99 | if (layerName == styleName) { 100 | return true; 101 | } 102 | } 103 | return false; 104 | } 105 | 106 | // collects all attributes from style layers 107 | var collectAttributes = function(layer){ 108 | var attributes = {}; 109 | 110 | // iterate over child layers to look for style layers 111 | var childLayers = [layer layers]; 112 | 113 | for (var i = 0; i < [childLayers count]; i++) { 114 | var styleLayer = childLayers[i]; 115 | 116 | //collect style layer 117 | if ([styleLayer name] == styleHandle + styleLayerName) { 118 | var styleLayerAttributes = attributesFromStyleLayer(styleLayer); 119 | for (var attr in styleLayerAttributes) { 120 | attributes[attr] = styleLayerAttributes[attr]; 121 | } 122 | } 123 | 124 | //collect compounds; 125 | for (var j = 0; j < compoundProperties.length; j++) { 126 | if ([styleLayer name] == styleHandle + compoundProperties[j]) { 127 | if (compoundProperties[j] == "size") { 128 | attributes["width"] = styleLayer.frame().width(); 129 | attributes["height"] = styleLayer.frame().height(); 130 | } else { 131 | var propertyTop = compoundProperties[j] + "Top"; 132 | var propertyBottom = compoundProperties[j] + "Bottom"; 133 | var propertyLeft = compoundProperties[j] + "Left"; 134 | var propertyRight = compoundProperties[j] + "Right"; 135 | 136 | attributes[propertyTop] = styleLayer.frame().height(); 137 | attributes[propertyBottom] = styleLayer.frame().height(); 138 | attributes[propertyRight] = styleLayer.frame().width(); 139 | attributes[propertyLeft] = styleLayer.frame().width(); 140 | } 141 | } 142 | }; 143 | 144 | //collect widths; 145 | for (var k = 0; k < widthProperties.length; k++) { 146 | var widthProperty = widthProperties[k]; 147 | if ([styleLayer name] == styleHandle + widthProperty) { 148 | attributes[widthProperty] = styleLayer.frame().width(); 149 | } 150 | } 151 | 152 | //collect heights; 153 | for (var l = 0; l < heightProperties.length; l++) { 154 | var heightProperty = heightProperties[l] 155 | if ([styleLayer name] == styleHandle + heightProperty) { 156 | attributes[heightProperty] = styleLayer.frame().height(); 157 | } 158 | } 159 | } 160 | return attributes; 161 | } 162 | 163 | //collects attribute from the styles text layer 164 | var attributesFromStyleLayer = function(layer){ 165 | var attributes = {}; 166 | if (utils.is.textLayer(layer)) { 167 | var attributeString = [layer stringValue]; 168 | //strip whitespace and newlines 169 | attributeString = [attributeString stringByReplacingOccurrencesOfString:"\n" withString:""]; 170 | attributeString = [attributeString stringByReplacingOccurrencesOfString:" " withString:""]; 171 | 172 | var attributesArray = [attributeString componentsSeparatedByString:";"]; 173 | for (var i = 0; i < [attributesArray count]; i++) { 174 | var currentAttribute = attributesArray[i]; 175 | var splitAttribute = [currentAttribute componentsSeparatedByString:":"]; 176 | if ([splitAttribute count] == 2) { 177 | var property = splitAttribute[0]; 178 | var propertyValue = splitAttribute[1]; 179 | attributes[property] = propertyValue; 180 | } 181 | } 182 | } 183 | return attributes; 184 | } 185 | 186 | // lays out and colors all the prototype style layers 187 | var layoutPrototypeStyles = function(layer){ 188 | var dimensionOffset = 4; // how far are the width layers offset from prototype 189 | var parentLayer = [layer parentGroup]; 190 | var stylesCenterVertically = ["paddingTop", "paddingBottom", "marginTop", "marginBottom", "width", "minWidth", "maxWidth"]; 191 | var stylesCenterHorizontally = ["paddingLeft", "paddingRight", "marginLeft", "marginRight", "height", "minHeight", "maxHeight"]; 192 | 193 | //TODO figure out a way to read the computed style, probably by saving the rect in 'computedStyle' valueforkey 194 | } 195 | 196 | // gets all prototype 197 | var getPrototypeLayers = function(){ 198 | var prototypes = []; 199 | utils.call.pageLayers(function(layer){ 200 | if (isLayerAPrototype(layer)) { 201 | var prototypeObj = {}; 202 | prototypeObj.name = getPrototypeClassFromLayer(layer); 203 | prototypeObj.layer = layer; 204 | prototypes.push(prototypeObj); 205 | } 206 | }); 207 | return prototypes; 208 | } 209 | 210 | // returns prototype class 211 | var getPrototypeClassFromLayer = function(layer){ 212 | var prototypeClass = ""; 213 | var prototypePrefix = prototypeKeyword + " "; 214 | return [[layer name] substringFromIndex:prototypePrefix.length]; 215 | } 216 | 217 | // returns layer class 218 | var getClassFromLayer = function(layer){ 219 | return "." + [[[layer name] componentsSeparatedByString:"."] lastObject]; 220 | } 221 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/lib/CSSJSON/cssjson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS-JSON Converter for JavaScript 3 | * Converts CSS to JSON and back. 4 | * Version 2.1 5 | * 6 | * Released under the MIT license. 7 | * 8 | * Copyright (c) 2013 Aram Kocharyan, http://aramk.com/ 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 11 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 13 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 16 | of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 19 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | var CSSJSON = new function () { 26 | 27 | var base = this; 28 | 29 | base.init = function () { 30 | // String functions 31 | String.prototype.trim = function () { 32 | return this.replace(/^\s+|\s+$/g, ''); 33 | }; 34 | 35 | String.prototype.repeat = function (n) { 36 | return new Array(1 + n).join(this); 37 | }; 38 | }; 39 | base.init(); 40 | 41 | var selX = /([^\s\;\{\}][^\;\{\}]*)\{/g; 42 | var endX = /\}/g; 43 | var lineX = /([^\;\{\}]*)\;/g; 44 | var commentX = /\/\*[\s\S]*?\*\//g; 45 | var lineAttrX = /([^\:]+):([^\;]*);/; 46 | 47 | // This is used, a concatenation of all above. We use alternation to 48 | // capture. 49 | var altX = /(\/\*[\s\S]*?\*\/)|([^\s\;\{\}][^\;\{\}]*(?=\{))|(\})|([^\;\{\}]+\;(?!\s*\*\/))/gmi; 50 | 51 | // Capture groups 52 | var capComment = 1; 53 | var capSelector = 2; 54 | var capEnd = 3; 55 | var capAttr = 4; 56 | 57 | var isEmpty = function (x) { 58 | return typeof x == 'undefined' || x.length == 0 || x == null; 59 | }; 60 | 61 | /** 62 | * Input is css string and current pos, returns JSON object 63 | * 64 | * @param cssString 65 | * The CSS string. 66 | * @param args 67 | * An optional argument object. ordered: Whether order of 68 | * comments and other nodes should be kept in the output. This 69 | * will return an object where all the keys are numbers and the 70 | * values are objects containing "name" and "value" keys for each 71 | * node. comments: Whether to capture comments. split: Whether to 72 | * split each comma separated list of selectors. 73 | */ 74 | base.toJSON = function (cssString, args) { 75 | var node = { 76 | children: {}, 77 | attributes: {} 78 | }; 79 | var match = null; 80 | var count = 0; 81 | 82 | if (typeof args == 'undefined') { 83 | var args = { 84 | ordered: false, 85 | comments: false, 86 | stripComments: false, 87 | split: false 88 | }; 89 | } 90 | if (args.stripComments) { 91 | args.comments = false; 92 | cssString = cssString.replace(commentX, ''); 93 | } 94 | 95 | while ((match = altX.exec(cssString)) != null) { 96 | if (!isEmpty(match[capComment]) && args.comments) { 97 | // Comment 98 | var add = match[capComment].trim(); 99 | node[count++] = add; 100 | } else if (!isEmpty(match[capSelector])) { 101 | // New node, we recurse 102 | var name = match[capSelector].trim(); 103 | // This will return when we encounter a closing brace 104 | var newNode = base.toJSON(cssString, args); 105 | if (args.ordered) { 106 | var obj = {}; 107 | obj['name'] = name; 108 | obj['value'] = newNode; 109 | // Since we must use key as index to keep order and not 110 | // name, this will differentiate between a Rule Node and an 111 | // Attribute, since both contain a name and value pair. 112 | obj['type'] = 'rule'; 113 | node[count++] = obj; 114 | } else { 115 | if (args.split) { 116 | var bits = name.split(','); 117 | } else { 118 | var bits = [name]; 119 | } 120 | for (i in bits) { 121 | var sel = bits[i].trim(); 122 | if (sel in node.children) { 123 | for (var att in newNode.attributes) { 124 | node.children[sel].attributes[att] = newNode.attributes[att]; 125 | } 126 | } else { 127 | node.children[sel] = newNode; 128 | } 129 | } 130 | } 131 | } else if (!isEmpty(match[capEnd])) { 132 | // Node has finished 133 | return node; 134 | } else if (!isEmpty(match[capAttr])) { 135 | var line = match[capAttr].trim(); 136 | var attr = lineAttrX.exec(line); 137 | if (attr) { 138 | // Attribute 139 | var name = attr[1].trim(); 140 | var value = attr[2].trim(); 141 | if (args.ordered) { 142 | var obj = {}; 143 | obj['name'] = name; 144 | obj['value'] = value; 145 | obj['type'] = 'attr'; 146 | node[count++] = obj; 147 | } else { 148 | if (name in node.attributes) { 149 | var currVal = node.attributes[name]; 150 | if (!(currVal instanceof Array)) { 151 | node.attributes[name] = [currVal]; 152 | } 153 | node.attributes[name].push(value); 154 | } else { 155 | node.attributes[name] = value; 156 | } 157 | } 158 | } else { 159 | // Semicolon terminated line 160 | node[count++] = line; 161 | } 162 | } 163 | } 164 | 165 | return node; 166 | }; 167 | 168 | /** 169 | * @param node 170 | * A JSON node. 171 | * @param depth 172 | * The depth of the current node; used for indentation and 173 | * optional. 174 | * @param breaks 175 | * Whether to add line breaks in the output. 176 | */ 177 | base.toCSS = function (node, depth, breaks) { 178 | var cssString = ''; 179 | if (typeof depth == 'undefined') { 180 | depth = 0; 181 | } 182 | if (typeof breaks == 'undefined') { 183 | breaks = false; 184 | } 185 | if (node.attributes) { 186 | for (i in node.attributes) { 187 | var att = node.attributes[i]; 188 | if (att instanceof Array) { 189 | for (var j = 0; j < att.length; j++) { 190 | cssString += strAttr(i, att[j], depth); 191 | } 192 | } else { 193 | cssString += strAttr(i, att, depth); 194 | } 195 | } 196 | } 197 | if (node.children) { 198 | var first = true; 199 | for (i in node.children) { 200 | if (breaks && !first) { 201 | cssString += '\n'; 202 | } else { 203 | first = false; 204 | } 205 | cssString += strNode(i, node.children[i], depth); 206 | } 207 | } 208 | return cssString; 209 | }; 210 | 211 | // Helpers 212 | 213 | var strAttr = function (name, value, depth) { 214 | return '\t'.repeat(depth) + name + ': ' + value + ';\n'; 215 | }; 216 | 217 | var strNode = function (name, value, depth) { 218 | var cssString = '\t'.repeat(depth) + name + ' {\n'; 219 | cssString += base.toCSS(value, depth + 1); 220 | cssString += '\t'.repeat(depth) + '}\n'; 221 | return cssString; 222 | }; 223 | 224 | }; 225 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/lib/CSSJSON/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | json2.js 3 | 2011-10-19 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, regexp: true */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | var JSON; 163 | if (!JSON) { 164 | JSON = {}; 165 | } 166 | 167 | (function () { 168 | 'use strict'; 169 | 170 | function f(n) { 171 | // Format integers to have at least two digits. 172 | return n < 10 ? '0' + n : n; 173 | } 174 | 175 | if (typeof Date.prototype.toJSON !== 'function') { 176 | 177 | Date.prototype.toJSON = function (key) { 178 | 179 | return isFinite(this.valueOf()) 180 | ? this.getUTCFullYear() + '-' + 181 | f(this.getUTCMonth() + 1) + '-' + 182 | f(this.getUTCDate()) + 'T' + 183 | f(this.getUTCHours()) + ':' + 184 | f(this.getUTCMinutes()) + ':' + 185 | f(this.getUTCSeconds()) + 'Z' 186 | : null; 187 | }; 188 | 189 | String.prototype.toJSON = 190 | Number.prototype.toJSON = 191 | Boolean.prototype.toJSON = function (key) { 192 | return this.valueOf(); 193 | }; 194 | } 195 | 196 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 197 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 198 | gap, 199 | indent, 200 | meta = { // table of character substitutions 201 | '\b': '\\b', 202 | '\t': '\\t', 203 | '\n': '\\n', 204 | '\f': '\\f', 205 | '\r': '\\r', 206 | '"' : '\\"', 207 | '\\': '\\\\' 208 | }, 209 | rep; 210 | 211 | 212 | function quote(string) { 213 | 214 | // If the string contains no control characters, no quote characters, and no 215 | // backslash characters, then we can safely slap some quotes around it. 216 | // Otherwise we must also replace the offending characters with safe escape 217 | // sequences. 218 | 219 | escapable.lastIndex = 0; 220 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 221 | var c = meta[a]; 222 | return typeof c === 'string' 223 | ? c 224 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 225 | }) + '"' : '"' + string + '"'; 226 | } 227 | 228 | 229 | function str(key, holder) { 230 | 231 | // Produce a string from holder[key]. 232 | 233 | var i, // The loop counter. 234 | k, // The member key. 235 | v, // The member value. 236 | length, 237 | mind = gap, 238 | partial, 239 | value = holder[key]; 240 | 241 | // If the value has a toJSON method, call it to obtain a replacement value. 242 | 243 | if (value && typeof value === 'object' && 244 | typeof value.toJSON === 'function') { 245 | value = value.toJSON(key); 246 | } 247 | 248 | // If we were called with a replacer function, then call the replacer to 249 | // obtain a replacement value. 250 | 251 | if (typeof rep === 'function') { 252 | value = rep.call(holder, key, value); 253 | } 254 | 255 | // What happens next depends on the value's type. 256 | 257 | switch (typeof value) { 258 | case 'string': 259 | return quote(value); 260 | 261 | case 'number': 262 | 263 | // JSON numbers must be finite. Encode non-finite numbers as null. 264 | 265 | return isFinite(value) ? String(value) : 'null'; 266 | 267 | case 'boolean': 268 | case 'null': 269 | 270 | // If the value is a boolean or null, convert it to a string. Note: 271 | // typeof null does not produce 'null'. The case is included here in 272 | // the remote chance that this gets fixed someday. 273 | 274 | return String(value); 275 | 276 | // If the type is 'object', we might be dealing with an object or an array or 277 | // null. 278 | 279 | case 'object': 280 | 281 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 282 | // so watch out for that case. 283 | 284 | if (!value) { 285 | return 'null'; 286 | } 287 | 288 | // Make an array to hold the partial results of stringifying this object value. 289 | 290 | gap += indent; 291 | partial = []; 292 | 293 | // Is the value an array? 294 | 295 | if (Object.prototype.toString.apply(value) === '[object Array]') { 296 | 297 | // The value is an array. Stringify every element. Use null as a placeholder 298 | // for non-JSON values. 299 | 300 | length = value.length; 301 | for (i = 0; i < length; i += 1) { 302 | partial[i] = str(i, value) || 'null'; 303 | } 304 | 305 | // Join all of the elements together, separated with commas, and wrap them in 306 | // brackets. 307 | 308 | v = partial.length === 0 309 | ? '[]' 310 | : gap 311 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 312 | : '[' + partial.join(',') + ']'; 313 | gap = mind; 314 | return v; 315 | } 316 | 317 | // If the replacer is an array, use it to select the members to be stringified. 318 | 319 | if (rep && typeof rep === 'object') { 320 | length = rep.length; 321 | for (i = 0; i < length; i += 1) { 322 | if (typeof rep[i] === 'string') { 323 | k = rep[i]; 324 | v = str(k, value); 325 | if (v) { 326 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 327 | } 328 | } 329 | } 330 | } else { 331 | 332 | // Otherwise, iterate through all of the keys in the object. 333 | 334 | for (k in value) { 335 | if (Object.prototype.hasOwnProperty.call(value, k)) { 336 | v = str(k, value); 337 | if (v) { 338 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 339 | } 340 | } 341 | } 342 | } 343 | 344 | // Join all of the member texts together, separated with commas, 345 | // and wrap them in braces. 346 | 347 | v = partial.length === 0 348 | ? '{}' 349 | : gap 350 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 351 | : '{' + partial.join(',') + '}'; 352 | gap = mind; 353 | return v; 354 | } 355 | } 356 | 357 | // If the JSON object does not yet have a stringify method, give it one. 358 | 359 | if (typeof JSON.stringify !== 'function') { 360 | JSON.stringify = function (value, replacer, space) { 361 | 362 | // The stringify method takes a value and an optional replacer, and an optional 363 | // space parameter, and returns a JSON text. The replacer can be a function 364 | // that can replace values, or an array of strings that will select the keys. 365 | // A default replacer method can be provided. Use of the space parameter can 366 | // produce text that is more easily readable. 367 | 368 | var i; 369 | gap = ''; 370 | indent = ''; 371 | 372 | // If the space parameter is a number, make an indent string containing that 373 | // many spaces. 374 | 375 | if (typeof space === 'number') { 376 | for (i = 0; i < space; i += 1) { 377 | indent += ' '; 378 | } 379 | 380 | // If the space parameter is a string, it will be used as the indent string. 381 | 382 | } else if (typeof space === 'string') { 383 | indent = space; 384 | } 385 | 386 | // If there is a replacer, it must be a function or an array. 387 | // Otherwise, throw an error. 388 | 389 | rep = replacer; 390 | if (replacer && typeof replacer !== 'function' && 391 | (typeof replacer !== 'object' || 392 | typeof replacer.length !== 'number')) { 393 | throw new Error('JSON.stringify'); 394 | } 395 | 396 | // Make a fake root object containing our value under the key of ''. 397 | // Return the result of stringifying the value. 398 | 399 | return str('', {'': value}); 400 | }; 401 | } 402 | 403 | 404 | // If the JSON object does not yet have a parse method, give it one. 405 | 406 | if (typeof JSON.parse !== 'function') { 407 | JSON.parse = function (text, reviver) { 408 | 409 | // The parse method takes a text and an optional reviver function, and returns 410 | // a JavaScript value if the text is a valid JSON text. 411 | 412 | var j; 413 | 414 | function walk(holder, key) { 415 | 416 | // The walk method is used to recursively walk the resulting structure so 417 | // that modifications can be made. 418 | 419 | var k, v, value = holder[key]; 420 | if (value && typeof value === 'object') { 421 | for (k in value) { 422 | if (Object.prototype.hasOwnProperty.call(value, k)) { 423 | v = walk(value, k); 424 | if (v !== undefined) { 425 | value[k] = v; 426 | } else { 427 | delete value[k]; 428 | } 429 | } 430 | } 431 | } 432 | return reviver.call(holder, key, value); 433 | } 434 | 435 | 436 | // Parsing happens in four stages. In the first stage, we replace certain 437 | // Unicode characters with escape sequences. JavaScript handles many characters 438 | // incorrectly, either silently deleting them, or treating them as line endings. 439 | 440 | text = String(text); 441 | cx.lastIndex = 0; 442 | if (cx.test(text)) { 443 | text = text.replace(cx, function (a) { 444 | return '\\u' + 445 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 446 | }); 447 | } 448 | 449 | // In the second stage, we run the text against regular expressions that look 450 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 451 | // because they can cause invocation, and '=' because it can cause mutation. 452 | // But just to be safe, we want to reject all unexpected forms. 453 | 454 | // We split the second stage into 4 regexp operations in order to work around 455 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 456 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 457 | // replace all simple value tokens with ']' characters. Third, we delete all 458 | // open brackets that follow a colon or comma or that begin the text. Finally, 459 | // we look to see that the remaining characters are only whitespace or ']' or 460 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 461 | 462 | if (/^[\],:{}\s]*$/ 463 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 464 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 465 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 466 | 467 | // In the third stage we use the eval function to compile the text into a 468 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 469 | // in JavaScript: it can begin a block or an object literal. We wrap the text 470 | // in parens to eliminate the ambiguity. 471 | 472 | j = eval('(' + text + ')'); 473 | 474 | // In the optional fourth stage, we recursively walk the new structure, passing 475 | // each name/value pair to a reviver function for possible transformation. 476 | 477 | return typeof reviver === 'function' 478 | ? walk({'': j}, '') 479 | : j; 480 | } 481 | 482 | // If the text is not JSON parseable, then a SyntaxError is thrown. 483 | 484 | throw new SyntaxError('JSON.parse'); 485 | }; 486 | } 487 | }()); 488 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/lib/css-layout/Layout-browserified.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.computeLayout = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 108 | layout.children = node.children.map(extractNodes); 109 | } else { 110 | delete node.children; 111 | } 112 | 113 | delete layout.right; 114 | delete layout.bottom; 115 | 116 | return layout; 117 | } 118 | 119 | function getPositiveSpacing(node, type, suffix, locations) { 120 | for (var i = 0; i < locations.length; ++i) { 121 | var location = locations[i]; 122 | 123 | var key = type + capitalizeFirst(location) + suffix; 124 | if (key in node.style && node.style[key] >= 0) { 125 | return node.style[key]; 126 | } 127 | 128 | key = type + suffix; 129 | if (key in node.style && node.style[key] >= 0) { 130 | return node.style[key]; 131 | } 132 | } 133 | 134 | return 0; 135 | } 136 | 137 | function isUndefined(value) { 138 | return value === undefined; 139 | } 140 | 141 | function isRowDirection(flexDirection) { 142 | return flexDirection === CSS_FLEX_DIRECTION_ROW || 143 | flexDirection === CSS_FLEX_DIRECTION_ROW_REVERSE; 144 | } 145 | 146 | function isColumnDirection(flexDirection) { 147 | return flexDirection === CSS_FLEX_DIRECTION_COLUMN || 148 | flexDirection === CSS_FLEX_DIRECTION_COLUMN_REVERSE; 149 | } 150 | 151 | function getLeadingLocations(axis) { 152 | var locations = [leading[axis]]; 153 | if (isRowDirection(axis)) { 154 | locations.unshift('start'); 155 | } 156 | 157 | return locations; 158 | } 159 | 160 | function getTrailingLocations(axis) { 161 | var locations = [trailing[axis]]; 162 | if (isRowDirection(axis)) { 163 | locations.unshift('end'); 164 | } 165 | 166 | return locations; 167 | } 168 | 169 | function getMargin(node, locations) { 170 | return getSpacing(node, 'margin', '', locations); 171 | } 172 | 173 | function getLeadingMargin(node, axis) { 174 | return getMargin(node, getLeadingLocations(axis)); 175 | } 176 | 177 | function getTrailingMargin(node, axis) { 178 | return getMargin(node, getTrailingLocations(axis)); 179 | } 180 | 181 | function getPadding(node, locations) { 182 | return getPositiveSpacing(node, 'padding', '', locations); 183 | } 184 | 185 | function getLeadingPadding(node, axis) { 186 | return getPadding(node, getLeadingLocations(axis)); 187 | } 188 | 189 | function getTrailingPadding(node, axis) { 190 | return getPadding(node, getTrailingLocations(axis)); 191 | } 192 | 193 | function getBorder(node, locations) { 194 | return getPositiveSpacing(node, 'border', 'Width', locations); 195 | } 196 | 197 | function getLeadingBorder(node, axis) { 198 | return getBorder(node, getLeadingLocations(axis)); 199 | } 200 | 201 | function getTrailingBorder(node, axis) { 202 | return getBorder(node, getTrailingLocations(axis)); 203 | } 204 | 205 | function getLeadingPaddingAndBorder(node, axis) { 206 | return getLeadingPadding(node, axis) + getLeadingBorder(node, axis); 207 | } 208 | 209 | function getTrailingPaddingAndBorder(node, axis) { 210 | return getTrailingPadding(node, axis) + getTrailingBorder(node, axis); 211 | } 212 | 213 | function getBorderAxis(node, axis) { 214 | return getLeadingBorder(node, axis) + getTrailingBorder(node, axis); 215 | } 216 | 217 | function getMarginAxis(node, axis) { 218 | return getLeadingMargin(node, axis) + getTrailingMargin(node, axis); 219 | } 220 | 221 | function getPaddingAndBorderAxis(node, axis) { 222 | return getLeadingPaddingAndBorder(node, axis) + 223 | getTrailingPaddingAndBorder(node, axis); 224 | } 225 | 226 | function getJustifyContent(node) { 227 | if ('justifyContent' in node.style) { 228 | return node.style.justifyContent; 229 | } 230 | return 'flex-start'; 231 | } 232 | 233 | function getAlignItem(node, child) { 234 | if ('alignSelf' in child.style) { 235 | return child.style.alignSelf; 236 | } 237 | if ('alignItems' in node.style) { 238 | return node.style.alignItems; 239 | } 240 | return 'stretch'; 241 | } 242 | 243 | function resolveAxis(axis, direction) { 244 | if (direction === CSS_DIRECTION_RTL) { 245 | if (axis === CSS_FLEX_DIRECTION_ROW) { 246 | return CSS_FLEX_DIRECTION_ROW_REVERSE; 247 | } else if (axis === CSS_FLEX_DIRECTION_ROW_REVERSE) { 248 | return CSS_FLEX_DIRECTION_ROW; 249 | } 250 | } 251 | 252 | return axis; 253 | } 254 | 255 | function resolveDirection(node, parentDirection) { 256 | var direction; 257 | if ('direction' in node.style) { 258 | direction = node.style.direction; 259 | } else { 260 | direction = CSS_DIRECTION_INHERIT; 261 | } 262 | 263 | if (direction === CSS_DIRECTION_INHERIT) { 264 | direction = (parentDirection === undefined ? CSS_DIRECTION_LTR : parentDirection); 265 | } 266 | 267 | return direction; 268 | } 269 | 270 | function getFlexDirection(node) { 271 | if ('flexDirection' in node.style) { 272 | return node.style.flexDirection; 273 | } 274 | return CSS_FLEX_DIRECTION_COLUMN; 275 | } 276 | 277 | function getCrossFlexDirection(flexDirection, direction) { 278 | if (isColumnDirection(flexDirection)) { 279 | return resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); 280 | } else { 281 | return CSS_FLEX_DIRECTION_COLUMN; 282 | } 283 | } 284 | 285 | function getPositionType(node) { 286 | if ('position' in node.style) { 287 | return node.style.position; 288 | } 289 | return 'relative'; 290 | } 291 | 292 | function getFlex(node) { 293 | return node.style.flex; 294 | } 295 | 296 | function isFlex(node) { 297 | return ( 298 | getPositionType(node) === CSS_POSITION_RELATIVE && 299 | getFlex(node) > 0 300 | ); 301 | } 302 | 303 | function isFlexWrap(node) { 304 | return node.style.flexWrap === 'wrap'; 305 | } 306 | 307 | function getDimWithMargin(node, axis) { 308 | return node.layout[dim[axis]] + getMarginAxis(node, axis); 309 | } 310 | 311 | function isDimDefined(node, axis) { 312 | return !isUndefined(node.style[dim[axis]]) && node.style[dim[axis]] >= 0; 313 | } 314 | 315 | function isPosDefined(node, pos) { 316 | return !isUndefined(node.style[pos]); 317 | } 318 | 319 | function isMeasureDefined(node) { 320 | return 'measure' in node.style; 321 | } 322 | 323 | function getPosition(node, pos) { 324 | if (pos in node.style) { 325 | return node.style[pos]; 326 | } 327 | return 0; 328 | } 329 | 330 | function boundAxis(node, axis, value) { 331 | var min = { 332 | 'row': node.style.minWidth, 333 | 'row-reverse': node.style.minWidth, 334 | 'column': node.style.minHeight, 335 | 'column-reverse': node.style.minHeight 336 | }[axis]; 337 | 338 | var max = { 339 | 'row': node.style.maxWidth, 340 | 'row-reverse': node.style.maxWidth, 341 | 'column': node.style.maxHeight, 342 | 'column-reverse': node.style.maxHeight 343 | }[axis]; 344 | 345 | var boundValue = value; 346 | if (!isUndefined(max) && max >= 0 && boundValue > max) { 347 | boundValue = max; 348 | } 349 | if (!isUndefined(min) && min >= 0 && boundValue < min) { 350 | boundValue = min; 351 | } 352 | return boundValue; 353 | } 354 | 355 | function fmaxf(a, b) { 356 | if (a > b) { 357 | return a; 358 | } 359 | return b; 360 | } 361 | 362 | // When the user specifically sets a value for width or height 363 | function setDimensionFromStyle(node, axis) { 364 | // The parent already computed us a width or height. We just skip it 365 | if (!isUndefined(node.layout[dim[axis]])) { 366 | return; 367 | } 368 | // We only run if there's a width or height defined 369 | if (!isDimDefined(node, axis)) { 370 | return; 371 | } 372 | 373 | // The dimensions can never be smaller than the padding and border 374 | node.layout[dim[axis]] = fmaxf( 375 | boundAxis(node, axis, node.style[dim[axis]]), 376 | getPaddingAndBorderAxis(node, axis) 377 | ); 378 | } 379 | 380 | function setTrailingPosition(node, child, axis) { 381 | child.layout[trailing[axis]] = node.layout[dim[axis]] - 382 | child.layout[dim[axis]] - child.layout[pos[axis]]; 383 | } 384 | 385 | // If both left and right are defined, then use left. Otherwise return 386 | // +left or -right depending on which is defined. 387 | function getRelativePosition(node, axis) { 388 | if (leading[axis] in node.style) { 389 | return getPosition(node, leading[axis]); 390 | } 391 | return -getPosition(node, trailing[axis]); 392 | } 393 | 394 | function layoutNode(node, parentMaxWidth, /*css_direction_t*/parentDirection) { 395 | var/*css_direction_t*/ direction = resolveDirection(node, parentDirection); 396 | var/*css_flex_direction_t*/ mainAxis = resolveAxis(getFlexDirection(node), direction); 397 | var/*css_flex_direction_t*/ crossAxis = getCrossFlexDirection(mainAxis, direction); 398 | var/*css_flex_direction_t*/ resolvedRowAxis = resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); 399 | 400 | // Handle width and height style attributes 401 | setDimensionFromStyle(node, mainAxis); 402 | setDimensionFromStyle(node, crossAxis); 403 | 404 | // The position is set by the parent, but we need to complete it with a 405 | // delta composed of the margin and left/top/right/bottom 406 | node.layout[leading[mainAxis]] += getLeadingMargin(node, mainAxis) + 407 | getRelativePosition(node, mainAxis); 408 | node.layout[trailing[mainAxis]] += getTrailingMargin(node, mainAxis) + 409 | getRelativePosition(node, mainAxis); 410 | node.layout[leading[crossAxis]] += getLeadingMargin(node, crossAxis) + 411 | getRelativePosition(node, crossAxis); 412 | node.layout[trailing[crossAxis]] += getTrailingMargin(node, crossAxis) + 413 | getRelativePosition(node, crossAxis); 414 | 415 | if (isMeasureDefined(node)) { 416 | var/*float*/ width = CSS_UNDEFINED; 417 | if (isDimDefined(node, resolvedRowAxis)) { 418 | width = node.style.width; 419 | } else if (!isUndefined(node.layout[dim[resolvedRowAxis]])) { 420 | width = node.layout[dim[resolvedRowAxis]]; 421 | } else { 422 | width = parentMaxWidth - 423 | getMarginAxis(node, resolvedRowAxis); 424 | } 425 | width -= getPaddingAndBorderAxis(node, resolvedRowAxis); 426 | 427 | // We only need to give a dimension for the text if we haven't got any 428 | // for it computed yet. It can either be from the style attribute or because 429 | // the element is flexible. 430 | var/*bool*/ isRowUndefined = !isDimDefined(node, resolvedRowAxis) && 431 | isUndefined(node.layout[dim[resolvedRowAxis]]); 432 | var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && 433 | isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]); 434 | 435 | // Let's not measure the text if we already know both dimensions 436 | if (isRowUndefined || isColumnUndefined) { 437 | var/*css_dim_t*/ measureDim = node.style.measure( 438 | /*(c)!node->context,*/ 439 | /*(java)!layoutContext.measureOutput,*/ 440 | width 441 | ); 442 | if (isRowUndefined) { 443 | node.layout.width = measureDim.width + 444 | getPaddingAndBorderAxis(node, resolvedRowAxis); 445 | } 446 | if (isColumnUndefined) { 447 | node.layout.height = measureDim.height + 448 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); 449 | } 450 | } 451 | if (node.children.length === 0) { 452 | return; 453 | } 454 | } 455 | 456 | var/*int*/ i; 457 | var/*int*/ ii; 458 | var/*css_node_t**/ child; 459 | var/*css_flex_direction_t*/ axis; 460 | 461 | // Pre-fill some dimensions straight from the parent 462 | for (i = 0; i < node.children.length; ++i) { 463 | child = node.children[i]; 464 | // Pre-fill cross axis dimensions when the child is using stretch before 465 | // we call the recursive layout pass 466 | if (getAlignItem(node, child) === CSS_ALIGN_STRETCH && 467 | getPositionType(child) === CSS_POSITION_RELATIVE && 468 | !isUndefined(node.layout[dim[crossAxis]]) && 469 | !isDimDefined(child, crossAxis)) { 470 | child.layout[dim[crossAxis]] = fmaxf( 471 | boundAxis(child, crossAxis, node.layout[dim[crossAxis]] - 472 | getPaddingAndBorderAxis(node, crossAxis) - 473 | getMarginAxis(child, crossAxis)), 474 | // You never want to go smaller than padding 475 | getPaddingAndBorderAxis(child, crossAxis) 476 | ); 477 | } else if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 478 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 479 | // left and right or top and bottom). 480 | for (ii = 0; ii < 2; ii++) { 481 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 482 | if (!isUndefined(node.layout[dim[axis]]) && 483 | !isDimDefined(child, axis) && 484 | isPosDefined(child, leading[axis]) && 485 | isPosDefined(child, trailing[axis])) { 486 | child.layout[dim[axis]] = fmaxf( 487 | boundAxis(child, axis, node.layout[dim[axis]] - 488 | getPaddingAndBorderAxis(node, axis) - 489 | getMarginAxis(child, axis) - 490 | getPosition(child, leading[axis]) - 491 | getPosition(child, trailing[axis])), 492 | // You never want to go smaller than padding 493 | getPaddingAndBorderAxis(child, axis) 494 | ); 495 | } 496 | } 497 | } 498 | } 499 | 500 | var/*float*/ definedMainDim = CSS_UNDEFINED; 501 | if (!isUndefined(node.layout[dim[mainAxis]])) { 502 | definedMainDim = node.layout[dim[mainAxis]] - 503 | getPaddingAndBorderAxis(node, mainAxis); 504 | } 505 | 506 | // We want to execute the next two loops one per line with flex-wrap 507 | var/*int*/ startLine = 0; 508 | var/*int*/ endLine = 0; 509 | // var/*int*/ nextOffset = 0; 510 | var/*int*/ alreadyComputedNextLayout = 0; 511 | // We aggregate the total dimensions of the container in those two variables 512 | var/*float*/ linesCrossDim = 0; 513 | var/*float*/ linesMainDim = 0; 514 | while (endLine < node.children.length) { 515 | // Layout non flexible children and count children by type 516 | 517 | // mainContentDim is accumulation of the dimensions and margin of all the 518 | // non flexible children. This will be used in order to either set the 519 | // dimensions of the node if none already exist, or to compute the 520 | // remaining space left for the flexible children. 521 | var/*float*/ mainContentDim = 0; 522 | 523 | // There are three kind of children, non flexible, flexible and absolute. 524 | // We need to know how many there are in order to distribute the space. 525 | var/*int*/ flexibleChildrenCount = 0; 526 | var/*float*/ totalFlexible = 0; 527 | var/*int*/ nonFlexibleChildrenCount = 0; 528 | 529 | var/*float*/ maxWidth; 530 | for (i = startLine; i < node.children.length; ++i) { 531 | child = node.children[i]; 532 | var/*float*/ nextContentDim = 0; 533 | 534 | // It only makes sense to consider a child flexible if we have a computed 535 | // dimension for the node. 536 | if (!isUndefined(node.layout[dim[mainAxis]]) && isFlex(child)) { 537 | flexibleChildrenCount++; 538 | totalFlexible += getFlex(child); 539 | 540 | // Even if we don't know its exact size yet, we already know the padding, 541 | // border and margin. We'll use this partial information, which represents 542 | // the smallest possible size for the child, to compute the remaining 543 | // available space. 544 | nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + 545 | getMarginAxis(child, mainAxis); 546 | 547 | } else { 548 | maxWidth = CSS_UNDEFINED; 549 | if (!isRowDirection(mainAxis)) { 550 | maxWidth = parentMaxWidth - 551 | getMarginAxis(node, resolvedRowAxis) - 552 | getPaddingAndBorderAxis(node, resolvedRowAxis); 553 | 554 | if (isDimDefined(node, resolvedRowAxis)) { 555 | maxWidth = node.layout[dim[resolvedRowAxis]] - 556 | getPaddingAndBorderAxis(node, resolvedRowAxis); 557 | } 558 | } 559 | 560 | // This is the main recursive call. We layout non flexible children. 561 | if (alreadyComputedNextLayout === 0) { 562 | layoutNode(/*(java)!layoutContext, */child, maxWidth, direction); 563 | } 564 | 565 | // Absolute positioned elements do not take part of the layout, so we 566 | // don't use them to compute mainContentDim 567 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 568 | nonFlexibleChildrenCount++; 569 | // At this point we know the final size and margin of the element. 570 | nextContentDim = getDimWithMargin(child, mainAxis); 571 | } 572 | } 573 | 574 | // The element we are about to add would make us go to the next line 575 | if (isFlexWrap(node) && 576 | !isUndefined(node.layout[dim[mainAxis]]) && 577 | mainContentDim + nextContentDim > definedMainDim && 578 | // If there's only one element, then it's bigger than the content 579 | // and needs its own line 580 | i !== startLine) { 581 | nonFlexibleChildrenCount--; 582 | alreadyComputedNextLayout = 1; 583 | break; 584 | } 585 | alreadyComputedNextLayout = 0; 586 | mainContentDim += nextContentDim; 587 | endLine = i + 1; 588 | } 589 | 590 | // Layout flexible children and allocate empty space 591 | 592 | // In order to position the elements in the main axis, we have two 593 | // controls. The space between the beginning and the first element 594 | // and the space between each two elements. 595 | var/*float*/ leadingMainDim = 0; 596 | var/*float*/ betweenMainDim = 0; 597 | 598 | // The remaining available space that needs to be allocated 599 | var/*float*/ remainingMainDim = 0; 600 | if (!isUndefined(node.layout[dim[mainAxis]])) { 601 | remainingMainDim = definedMainDim - mainContentDim; 602 | } else { 603 | remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim; 604 | } 605 | 606 | // If there are flexible children in the mix, they are going to fill the 607 | // remaining space 608 | if (flexibleChildrenCount !== 0) { 609 | var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible; 610 | var/*float*/ baseMainDim; 611 | var/*float*/ boundMainDim; 612 | 613 | // Iterate over every child in the axis. If the flex share of remaining 614 | // space doesn't meet min/max bounds, remove this child from flex 615 | // calculations. 616 | for (i = startLine; i < endLine; ++i) { 617 | child = node.children[i]; 618 | if (isFlex(child)) { 619 | baseMainDim = flexibleMainDim * getFlex(child) + 620 | getPaddingAndBorderAxis(child, mainAxis); 621 | boundMainDim = boundAxis(child, mainAxis, baseMainDim); 622 | 623 | if (baseMainDim !== boundMainDim) { 624 | remainingMainDim -= boundMainDim; 625 | totalFlexible -= getFlex(child); 626 | } 627 | } 628 | } 629 | flexibleMainDim = remainingMainDim / totalFlexible; 630 | 631 | // The non flexible children can overflow the container, in this case 632 | // we should just assume that there is no space available. 633 | if (flexibleMainDim < 0) { 634 | flexibleMainDim = 0; 635 | } 636 | // We iterate over the full array and only apply the action on flexible 637 | // children. This is faster than actually allocating a new array that 638 | // contains only flexible children. 639 | for (i = startLine; i < endLine; ++i) { 640 | child = node.children[i]; 641 | if (isFlex(child)) { 642 | // At this point we know the final size of the element in the main 643 | // dimension 644 | child.layout[dim[mainAxis]] = boundAxis(child, mainAxis, 645 | flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) 646 | ); 647 | 648 | maxWidth = CSS_UNDEFINED; 649 | if (isDimDefined(node, resolvedRowAxis)) { 650 | maxWidth = node.layout[dim[resolvedRowAxis]] - 651 | getPaddingAndBorderAxis(node, resolvedRowAxis); 652 | } else if (!isRowDirection(mainAxis)) { 653 | maxWidth = parentMaxWidth - 654 | getMarginAxis(node, resolvedRowAxis) - 655 | getPaddingAndBorderAxis(node, resolvedRowAxis); 656 | } 657 | 658 | // And we recursively call the layout algorithm for this child 659 | layoutNode(/*(java)!layoutContext, */child, maxWidth, direction); 660 | } 661 | } 662 | 663 | // We use justifyContent to figure out how to allocate the remaining 664 | // space available 665 | } else { 666 | var/*css_justify_t*/ justifyContent = getJustifyContent(node); 667 | if (justifyContent === CSS_JUSTIFY_CENTER) { 668 | leadingMainDim = remainingMainDim / 2; 669 | } else if (justifyContent === CSS_JUSTIFY_FLEX_END) { 670 | leadingMainDim = remainingMainDim; 671 | } else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) { 672 | remainingMainDim = fmaxf(remainingMainDim, 0); 673 | if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) { 674 | betweenMainDim = remainingMainDim / 675 | (flexibleChildrenCount + nonFlexibleChildrenCount - 1); 676 | } else { 677 | betweenMainDim = 0; 678 | } 679 | } else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) { 680 | // Space on the edges is half of the space between elements 681 | betweenMainDim = remainingMainDim / 682 | (flexibleChildrenCount + nonFlexibleChildrenCount); 683 | leadingMainDim = betweenMainDim / 2; 684 | } 685 | } 686 | 687 | // Position elements in the main axis and compute dimensions 688 | 689 | // At this point, all the children have their dimensions set. We need to 690 | // find their position. In order to do that, we accumulate data in 691 | // variables that are also useful to compute the total dimensions of the 692 | // container! 693 | var/*float*/ crossDim = 0; 694 | var/*float*/ mainDim = leadingMainDim + 695 | getLeadingPaddingAndBorder(node, mainAxis); 696 | 697 | for (i = startLine; i < endLine; ++i) { 698 | child = node.children[i]; 699 | 700 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 701 | isPosDefined(child, leading[mainAxis])) { 702 | // In case the child is position absolute and has left/top being 703 | // defined, we override the position to whatever the user said 704 | // (and margin/border). 705 | child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + 706 | getLeadingBorder(node, mainAxis) + 707 | getLeadingMargin(child, mainAxis); 708 | } else { 709 | // If the child is position absolute (without top/left) or relative, 710 | // we put it at the current accumulated offset. 711 | child.layout[pos[mainAxis]] += mainDim; 712 | 713 | // Define the trailing position accordingly. 714 | if (!isUndefined(node.layout[dim[mainAxis]])) { 715 | setTrailingPosition(node, child, mainAxis); 716 | } 717 | } 718 | 719 | // Now that we placed the element, we need to update the variables 720 | // We only need to do that for relative elements. Absolute elements 721 | // do not take part in that phase. 722 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 723 | // The main dimension is the sum of all the elements dimension plus 724 | // the spacing. 725 | mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); 726 | // The cross dimension is the max of the elements dimension since there 727 | // can only be one element in that cross dimension. 728 | crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); 729 | } 730 | } 731 | 732 | var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]]; 733 | if (isUndefined(node.layout[dim[crossAxis]])) { 734 | containerCrossAxis = fmaxf( 735 | // For the cross dim, we add both sides at the end because the value 736 | // is aggregate via a max function. Intermediate negative values 737 | // can mess this computation otherwise 738 | boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), 739 | getPaddingAndBorderAxis(node, crossAxis) 740 | ); 741 | } 742 | 743 | // Position elements in the cross axis 744 | 745 | for (i = startLine; i < endLine; ++i) { 746 | child = node.children[i]; 747 | 748 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 749 | isPosDefined(child, leading[crossAxis])) { 750 | // In case the child is absolutely positionned and has a 751 | // top/left/bottom/right being set, we override all the previously 752 | // computed positions to set it correctly. 753 | child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + 754 | getLeadingBorder(node, crossAxis) + 755 | getLeadingMargin(child, crossAxis); 756 | 757 | } else { 758 | var/*float*/ leadingCrossDim = getLeadingPaddingAndBorder(node, crossAxis); 759 | 760 | // For a relative children, we're either using alignItems (parent) or 761 | // alignSelf (child) in order to determine the position in the cross axis 762 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 763 | var/*css_align_t*/ alignItem = getAlignItem(node, child); 764 | if (alignItem === CSS_ALIGN_STRETCH) { 765 | // You can only stretch if the dimension has not already been set 766 | // previously. 767 | if (!isDimDefined(child, crossAxis)) { 768 | child.layout[dim[crossAxis]] = fmaxf( 769 | boundAxis(child, crossAxis, containerCrossAxis - 770 | getPaddingAndBorderAxis(node, crossAxis) - 771 | getMarginAxis(child, crossAxis)), 772 | // You never want to go smaller than padding 773 | getPaddingAndBorderAxis(child, crossAxis) 774 | ); 775 | } 776 | } else if (alignItem !== CSS_ALIGN_FLEX_START) { 777 | // The remaining space between the parent dimensions+padding and child 778 | // dimensions+margin. 779 | var/*float*/ remainingCrossDim = containerCrossAxis - 780 | getPaddingAndBorderAxis(node, crossAxis) - 781 | getDimWithMargin(child, crossAxis); 782 | 783 | if (alignItem === CSS_ALIGN_CENTER) { 784 | leadingCrossDim += remainingCrossDim / 2; 785 | } else { // CSS_ALIGN_FLEX_END 786 | leadingCrossDim += remainingCrossDim; 787 | } 788 | } 789 | } 790 | 791 | // And we apply the position 792 | child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim; 793 | 794 | // Define the trailing position accordingly. 795 | if (!isUndefined(node.layout[dim[crossAxis]])) { 796 | setTrailingPosition(node, child, crossAxis); 797 | } 798 | } 799 | } 800 | 801 | linesCrossDim += crossDim; 802 | linesMainDim = fmaxf(linesMainDim, mainDim); 803 | startLine = endLine; 804 | } 805 | 806 | var/*bool*/ needsMainTrailingPos = false; 807 | var/*bool*/ needsCrossTrailingPos = false; 808 | 809 | // If the user didn't specify a width or height, and it has not been set 810 | // by the container, then we set it via the children. 811 | if (isUndefined(node.layout[dim[mainAxis]])) { 812 | node.layout[dim[mainAxis]] = fmaxf( 813 | // We're missing the last padding at this point to get the final 814 | // dimension 815 | boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)), 816 | // We can never assign a width smaller than the padding and borders 817 | getPaddingAndBorderAxis(node, mainAxis) 818 | ); 819 | 820 | needsMainTrailingPos = true; 821 | } 822 | 823 | if (isUndefined(node.layout[dim[crossAxis]])) { 824 | node.layout[dim[crossAxis]] = fmaxf( 825 | // For the cross dim, we add both sides at the end because the value 826 | // is aggregate via a max function. Intermediate negative values 827 | // can mess this computation otherwise 828 | boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), 829 | getPaddingAndBorderAxis(node, crossAxis) 830 | ); 831 | 832 | needsCrossTrailingPos = true; 833 | } 834 | 835 | // Set trailing position if necessary 836 | 837 | if (needsMainTrailingPos || needsCrossTrailingPos) { 838 | for (i = 0; i < node.children.length; ++i) { 839 | child = node.children[i]; 840 | 841 | if (needsMainTrailingPos) { 842 | setTrailingPosition(node, child, mainAxis); 843 | } 844 | 845 | if (needsCrossTrailingPos) { 846 | setTrailingPosition(node, child, crossAxis); 847 | } 848 | } 849 | } 850 | 851 | // Calculate dimensions for absolutely positioned elements 852 | 853 | for (i = 0; i < node.children.length; ++i) { 854 | child = node.children[i]; 855 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 856 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 857 | // left and right or top and bottom). 858 | for (ii = 0; ii < 2; ii++) { 859 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 860 | if (!isUndefined(node.layout[dim[axis]]) && 861 | !isDimDefined(child, axis) && 862 | isPosDefined(child, leading[axis]) && 863 | isPosDefined(child, trailing[axis])) { 864 | child.layout[dim[axis]] = fmaxf( 865 | boundAxis(child, axis, node.layout[dim[axis]] - 866 | getBorderAxis(node, axis) - 867 | getMarginAxis(child, axis) - 868 | getPosition(child, leading[axis]) - 869 | getPosition(child, trailing[axis]) 870 | ), 871 | // You never want to go smaller than padding 872 | getPaddingAndBorderAxis(child, axis) 873 | ); 874 | } 875 | } 876 | for (ii = 0; ii < 2; ii++) { 877 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 878 | if (isPosDefined(child, trailing[axis]) && 879 | !isPosDefined(child, leading[axis])) { 880 | child.layout[leading[axis]] = 881 | node.layout[dim[axis]] - 882 | child.layout[dim[axis]] - 883 | getPosition(child, trailing[axis]); 884 | } 885 | } 886 | } 887 | } 888 | } 889 | 890 | return { 891 | computeLayout: layoutNode, 892 | fillNodes: fillNodes, 893 | extractNodes: extractNodes 894 | }; 895 | })(); 896 | 897 | // UMD (Universal Module Definition) 898 | // See https://github.com/umdjs/umd for reference 899 | (function (root, factory) { 900 | if (typeof define === 'function' && define.amd) { 901 | // AMD. Register as an anonymous module. 902 | define([], factory); 903 | } else if (typeof exports === 'object') { 904 | // Node. Does not work with strict CommonJS, but 905 | // only CommonJS-like environments that support module.exports, 906 | // like Node. 907 | module.exports = factory(); 908 | } else { 909 | // Browser globals (root is window) 910 | root.returnExports = factory(); 911 | } 912 | }(this, function () { 913 | return computeLayout; 914 | })); 915 | 916 | },{}]},{},[1])(1) 917 | }); -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/lib/css-layout/Layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | var computeLayout = (function() { 11 | 12 | var CSS_UNDEFINED; 13 | 14 | var CSS_DIRECTION_INHERIT = 'inherit'; 15 | var CSS_DIRECTION_LTR = 'ltr'; 16 | var CSS_DIRECTION_RTL = 'rtl'; 17 | 18 | var CSS_FLEX_DIRECTION_ROW = 'row'; 19 | var CSS_FLEX_DIRECTION_ROW_REVERSE = 'row-reverse'; 20 | var CSS_FLEX_DIRECTION_COLUMN = 'column'; 21 | var CSS_FLEX_DIRECTION_COLUMN_REVERSE = 'column-reverse'; 22 | 23 | // var CSS_JUSTIFY_FLEX_START = 'flex-start'; 24 | var CSS_JUSTIFY_CENTER = 'center'; 25 | var CSS_JUSTIFY_FLEX_END = 'flex-end'; 26 | var CSS_JUSTIFY_SPACE_BETWEEN = 'space-between'; 27 | var CSS_JUSTIFY_SPACE_AROUND = 'space-around'; 28 | 29 | var CSS_ALIGN_FLEX_START = 'flex-start'; 30 | var CSS_ALIGN_CENTER = 'center'; 31 | // var CSS_ALIGN_FLEX_END = 'flex-end'; 32 | var CSS_ALIGN_STRETCH = 'stretch'; 33 | 34 | var CSS_POSITION_RELATIVE = 'relative'; 35 | var CSS_POSITION_ABSOLUTE = 'absolute'; 36 | 37 | var leading = { 38 | 'row': 'left', 39 | 'row-reverse': 'right', 40 | 'column': 'top', 41 | 'column-reverse': 'bottom' 42 | }; 43 | var trailing = { 44 | 'row': 'right', 45 | 'row-reverse': 'left', 46 | 'column': 'bottom', 47 | 'column-reverse': 'top' 48 | }; 49 | var pos = { 50 | 'row': 'left', 51 | 'row-reverse': 'right', 52 | 'column': 'top', 53 | 'column-reverse': 'bottom' 54 | }; 55 | var dim = { 56 | 'row': 'width', 57 | 'row-reverse': 'width', 58 | 'column': 'height', 59 | 'column-reverse': 'height' 60 | }; 61 | 62 | function capitalizeFirst(str) { 63 | return str.charAt(0).toUpperCase() + str.slice(1); 64 | } 65 | 66 | function getSpacing(node, type, suffix, locations) { 67 | for (var i = 0; i < locations.length; ++i) { 68 | var location = locations[i]; 69 | 70 | var key = type + capitalizeFirst(location) + suffix; 71 | if (key in node.style) { 72 | return node.style[key]; 73 | } 74 | 75 | key = type + suffix; 76 | if (key in node.style) { 77 | return node.style[key]; 78 | } 79 | } 80 | 81 | return 0; 82 | } 83 | function fillNodes(node) { 84 | node.layout = { 85 | width: undefined, 86 | height: undefined, 87 | top: 0, 88 | left: 0, 89 | right: 0, 90 | bottom: 0 91 | }; 92 | if (!node.style) { 93 | node.style = {}; 94 | } 95 | 96 | if (!node.children || node.style.measure) { 97 | node.children = []; 98 | } 99 | node.children.forEach(fillNodes); 100 | return node; 101 | } 102 | 103 | function extractNodes(node) { 104 | var layout = node.layout; 105 | delete node.layout; 106 | if (node.children && node.children.length > 0) { 107 | layout.children = node.children.map(extractNodes); 108 | } else { 109 | delete node.children; 110 | } 111 | 112 | delete layout.right; 113 | delete layout.bottom; 114 | 115 | return layout; 116 | } 117 | 118 | function getPositiveSpacing(node, type, suffix, locations) { 119 | for (var i = 0; i < locations.length; ++i) { 120 | var location = locations[i]; 121 | 122 | var key = type + capitalizeFirst(location) + suffix; 123 | if (key in node.style && node.style[key] >= 0) { 124 | return node.style[key]; 125 | } 126 | 127 | key = type + suffix; 128 | if (key in node.style && node.style[key] >= 0) { 129 | return node.style[key]; 130 | } 131 | } 132 | 133 | return 0; 134 | } 135 | 136 | function isUndefined(value) { 137 | return value === undefined; 138 | } 139 | 140 | function isRowDirection(flexDirection) { 141 | return flexDirection === CSS_FLEX_DIRECTION_ROW || 142 | flexDirection === CSS_FLEX_DIRECTION_ROW_REVERSE; 143 | } 144 | 145 | function isColumnDirection(flexDirection) { 146 | return flexDirection === CSS_FLEX_DIRECTION_COLUMN || 147 | flexDirection === CSS_FLEX_DIRECTION_COLUMN_REVERSE; 148 | } 149 | 150 | function getLeadingLocations(axis) { 151 | var locations = [leading[axis]]; 152 | if (isRowDirection(axis)) { 153 | locations.unshift('start'); 154 | } 155 | 156 | return locations; 157 | } 158 | 159 | function getTrailingLocations(axis) { 160 | var locations = [trailing[axis]]; 161 | if (isRowDirection(axis)) { 162 | locations.unshift('end'); 163 | } 164 | 165 | return locations; 166 | } 167 | 168 | function getMargin(node, locations) { 169 | return getSpacing(node, 'margin', '', locations); 170 | } 171 | 172 | function getLeadingMargin(node, axis) { 173 | return getMargin(node, getLeadingLocations(axis)); 174 | } 175 | 176 | function getTrailingMargin(node, axis) { 177 | return getMargin(node, getTrailingLocations(axis)); 178 | } 179 | 180 | function getPadding(node, locations) { 181 | return getPositiveSpacing(node, 'padding', '', locations); 182 | } 183 | 184 | function getLeadingPadding(node, axis) { 185 | return getPadding(node, getLeadingLocations(axis)); 186 | } 187 | 188 | function getTrailingPadding(node, axis) { 189 | return getPadding(node, getTrailingLocations(axis)); 190 | } 191 | 192 | function getBorder(node, locations) { 193 | return getPositiveSpacing(node, 'border', 'Width', locations); 194 | } 195 | 196 | function getLeadingBorder(node, axis) { 197 | return getBorder(node, getLeadingLocations(axis)); 198 | } 199 | 200 | function getTrailingBorder(node, axis) { 201 | return getBorder(node, getTrailingLocations(axis)); 202 | } 203 | 204 | function getLeadingPaddingAndBorder(node, axis) { 205 | return getLeadingPadding(node, axis) + getLeadingBorder(node, axis); 206 | } 207 | 208 | function getTrailingPaddingAndBorder(node, axis) { 209 | return getTrailingPadding(node, axis) + getTrailingBorder(node, axis); 210 | } 211 | 212 | function getBorderAxis(node, axis) { 213 | return getLeadingBorder(node, axis) + getTrailingBorder(node, axis); 214 | } 215 | 216 | function getMarginAxis(node, axis) { 217 | return getLeadingMargin(node, axis) + getTrailingMargin(node, axis); 218 | } 219 | 220 | function getPaddingAndBorderAxis(node, axis) { 221 | return getLeadingPaddingAndBorder(node, axis) + 222 | getTrailingPaddingAndBorder(node, axis); 223 | } 224 | 225 | function getJustifyContent(node) { 226 | if ('justifyContent' in node.style) { 227 | return node.style.justifyContent; 228 | } 229 | return 'flex-start'; 230 | } 231 | 232 | function getAlignItem(node, child) { 233 | if ('alignSelf' in child.style) { 234 | return child.style.alignSelf; 235 | } 236 | if ('alignItems' in node.style) { 237 | return node.style.alignItems; 238 | } 239 | return 'stretch'; 240 | } 241 | 242 | function resolveAxis(axis, direction) { 243 | if (direction === CSS_DIRECTION_RTL) { 244 | if (axis === CSS_FLEX_DIRECTION_ROW) { 245 | return CSS_FLEX_DIRECTION_ROW_REVERSE; 246 | } else if (axis === CSS_FLEX_DIRECTION_ROW_REVERSE) { 247 | return CSS_FLEX_DIRECTION_ROW; 248 | } 249 | } 250 | 251 | return axis; 252 | } 253 | 254 | function resolveDirection(node, parentDirection) { 255 | var direction; 256 | if ('direction' in node.style) { 257 | direction = node.style.direction; 258 | } else { 259 | direction = CSS_DIRECTION_INHERIT; 260 | } 261 | 262 | if (direction === CSS_DIRECTION_INHERIT) { 263 | direction = (parentDirection === undefined ? CSS_DIRECTION_LTR : parentDirection); 264 | } 265 | 266 | return direction; 267 | } 268 | 269 | function getFlexDirection(node) { 270 | if ('flexDirection' in node.style) { 271 | return node.style.flexDirection; 272 | } 273 | return CSS_FLEX_DIRECTION_COLUMN; 274 | } 275 | 276 | function getCrossFlexDirection(flexDirection, direction) { 277 | if (isColumnDirection(flexDirection)) { 278 | return resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); 279 | } else { 280 | return CSS_FLEX_DIRECTION_COLUMN; 281 | } 282 | } 283 | 284 | function getPositionType(node) { 285 | if ('position' in node.style) { 286 | return node.style.position; 287 | } 288 | return 'relative'; 289 | } 290 | 291 | function getFlex(node) { 292 | return node.style.flex; 293 | } 294 | 295 | function isFlex(node) { 296 | return ( 297 | getPositionType(node) === CSS_POSITION_RELATIVE && 298 | getFlex(node) > 0 299 | ); 300 | } 301 | 302 | function isFlexWrap(node) { 303 | return node.style.flexWrap === 'wrap'; 304 | } 305 | 306 | function getDimWithMargin(node, axis) { 307 | return node.layout[dim[axis]] + getMarginAxis(node, axis); 308 | } 309 | 310 | function isDimDefined(node, axis) { 311 | return !isUndefined(node.style[dim[axis]]) && node.style[dim[axis]] >= 0; 312 | } 313 | 314 | function isPosDefined(node, pos) { 315 | return !isUndefined(node.style[pos]); 316 | } 317 | 318 | function isMeasureDefined(node) { 319 | return 'measure' in node.style; 320 | } 321 | 322 | function getPosition(node, pos) { 323 | if (pos in node.style) { 324 | return node.style[pos]; 325 | } 326 | return 0; 327 | } 328 | 329 | function boundAxis(node, axis, value) { 330 | var min = { 331 | 'row': node.style.minWidth, 332 | 'row-reverse': node.style.minWidth, 333 | 'column': node.style.minHeight, 334 | 'column-reverse': node.style.minHeight 335 | }[axis]; 336 | 337 | var max = { 338 | 'row': node.style.maxWidth, 339 | 'row-reverse': node.style.maxWidth, 340 | 'column': node.style.maxHeight, 341 | 'column-reverse': node.style.maxHeight 342 | }[axis]; 343 | 344 | var boundValue = value; 345 | if (!isUndefined(max) && max >= 0 && boundValue > max) { 346 | boundValue = max; 347 | } 348 | if (!isUndefined(min) && min >= 0 && boundValue < min) { 349 | boundValue = min; 350 | } 351 | return boundValue; 352 | } 353 | 354 | function fmaxf(a, b) { 355 | if (a > b) { 356 | return a; 357 | } 358 | return b; 359 | } 360 | 361 | // When the user specifically sets a value for width or height 362 | function setDimensionFromStyle(node, axis) { 363 | // The parent already computed us a width or height. We just skip it 364 | if (!isUndefined(node.layout[dim[axis]])) { 365 | return; 366 | } 367 | // We only run if there's a width or height defined 368 | if (!isDimDefined(node, axis)) { 369 | return; 370 | } 371 | 372 | // The dimensions can never be smaller than the padding and border 373 | node.layout[dim[axis]] = fmaxf( 374 | boundAxis(node, axis, node.style[dim[axis]]), 375 | getPaddingAndBorderAxis(node, axis) 376 | ); 377 | } 378 | 379 | function setTrailingPosition(node, child, axis) { 380 | child.layout[trailing[axis]] = node.layout[dim[axis]] - 381 | child.layout[dim[axis]] - child.layout[pos[axis]]; 382 | } 383 | 384 | // If both left and right are defined, then use left. Otherwise return 385 | // +left or -right depending on which is defined. 386 | function getRelativePosition(node, axis) { 387 | if (leading[axis] in node.style) { 388 | return getPosition(node, leading[axis]); 389 | } 390 | return -getPosition(node, trailing[axis]); 391 | } 392 | 393 | function layoutNode(node, parentMaxWidth, /*css_direction_t*/parentDirection) { 394 | var/*css_direction_t*/ direction = resolveDirection(node, parentDirection); 395 | var/*css_flex_direction_t*/ mainAxis = resolveAxis(getFlexDirection(node), direction); 396 | var/*css_flex_direction_t*/ crossAxis = getCrossFlexDirection(mainAxis, direction); 397 | var/*css_flex_direction_t*/ resolvedRowAxis = resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); 398 | 399 | // Handle width and height style attributes 400 | setDimensionFromStyle(node, mainAxis); 401 | setDimensionFromStyle(node, crossAxis); 402 | 403 | // The position is set by the parent, but we need to complete it with a 404 | // delta composed of the margin and left/top/right/bottom 405 | node.layout[leading[mainAxis]] += getLeadingMargin(node, mainAxis) + 406 | getRelativePosition(node, mainAxis); 407 | node.layout[trailing[mainAxis]] += getTrailingMargin(node, mainAxis) + 408 | getRelativePosition(node, mainAxis); 409 | node.layout[leading[crossAxis]] += getLeadingMargin(node, crossAxis) + 410 | getRelativePosition(node, crossAxis); 411 | node.layout[trailing[crossAxis]] += getTrailingMargin(node, crossAxis) + 412 | getRelativePosition(node, crossAxis); 413 | 414 | if (isMeasureDefined(node)) { 415 | var/*float*/ width = CSS_UNDEFINED; 416 | if (isDimDefined(node, resolvedRowAxis)) { 417 | width = node.style.width; 418 | } else if (!isUndefined(node.layout[dim[resolvedRowAxis]])) { 419 | width = node.layout[dim[resolvedRowAxis]]; 420 | } else { 421 | width = parentMaxWidth - 422 | getMarginAxis(node, resolvedRowAxis); 423 | } 424 | width -= getPaddingAndBorderAxis(node, resolvedRowAxis); 425 | 426 | // We only need to give a dimension for the text if we haven't got any 427 | // for it computed yet. It can either be from the style attribute or because 428 | // the element is flexible. 429 | var/*bool*/ isRowUndefined = !isDimDefined(node, resolvedRowAxis) && 430 | isUndefined(node.layout[dim[resolvedRowAxis]]); 431 | var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && 432 | isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]); 433 | 434 | // Let's not measure the text if we already know both dimensions 435 | if (isRowUndefined || isColumnUndefined) { 436 | var/*css_dim_t*/ measureDim = node.style.measure( 437 | /*(c)!node->context,*/ 438 | /*(java)!layoutContext.measureOutput,*/ 439 | width 440 | ); 441 | if (isRowUndefined) { 442 | node.layout.width = measureDim.width + 443 | getPaddingAndBorderAxis(node, resolvedRowAxis); 444 | } 445 | if (isColumnUndefined) { 446 | node.layout.height = measureDim.height + 447 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); 448 | } 449 | } 450 | if (node.children.length === 0) { 451 | return; 452 | } 453 | } 454 | 455 | var/*int*/ i; 456 | var/*int*/ ii; 457 | var/*css_node_t**/ child; 458 | var/*css_flex_direction_t*/ axis; 459 | 460 | // Pre-fill some dimensions straight from the parent 461 | for (i = 0; i < node.children.length; ++i) { 462 | child = node.children[i]; 463 | // Pre-fill cross axis dimensions when the child is using stretch before 464 | // we call the recursive layout pass 465 | if (getAlignItem(node, child) === CSS_ALIGN_STRETCH && 466 | getPositionType(child) === CSS_POSITION_RELATIVE && 467 | !isUndefined(node.layout[dim[crossAxis]]) && 468 | !isDimDefined(child, crossAxis)) { 469 | child.layout[dim[crossAxis]] = fmaxf( 470 | boundAxis(child, crossAxis, node.layout[dim[crossAxis]] - 471 | getPaddingAndBorderAxis(node, crossAxis) - 472 | getMarginAxis(child, crossAxis)), 473 | // You never want to go smaller than padding 474 | getPaddingAndBorderAxis(child, crossAxis) 475 | ); 476 | } else if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 477 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 478 | // left and right or top and bottom). 479 | for (ii = 0; ii < 2; ii++) { 480 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 481 | if (!isUndefined(node.layout[dim[axis]]) && 482 | !isDimDefined(child, axis) && 483 | isPosDefined(child, leading[axis]) && 484 | isPosDefined(child, trailing[axis])) { 485 | child.layout[dim[axis]] = fmaxf( 486 | boundAxis(child, axis, node.layout[dim[axis]] - 487 | getPaddingAndBorderAxis(node, axis) - 488 | getMarginAxis(child, axis) - 489 | getPosition(child, leading[axis]) - 490 | getPosition(child, trailing[axis])), 491 | // You never want to go smaller than padding 492 | getPaddingAndBorderAxis(child, axis) 493 | ); 494 | } 495 | } 496 | } 497 | } 498 | 499 | var/*float*/ definedMainDim = CSS_UNDEFINED; 500 | if (!isUndefined(node.layout[dim[mainAxis]])) { 501 | definedMainDim = node.layout[dim[mainAxis]] - 502 | getPaddingAndBorderAxis(node, mainAxis); 503 | } 504 | 505 | // We want to execute the next two loops one per line with flex-wrap 506 | var/*int*/ startLine = 0; 507 | var/*int*/ endLine = 0; 508 | // var/*int*/ nextOffset = 0; 509 | var/*int*/ alreadyComputedNextLayout = 0; 510 | // We aggregate the total dimensions of the container in those two variables 511 | var/*float*/ linesCrossDim = 0; 512 | var/*float*/ linesMainDim = 0; 513 | while (endLine < node.children.length) { 514 | // Layout non flexible children and count children by type 515 | 516 | // mainContentDim is accumulation of the dimensions and margin of all the 517 | // non flexible children. This will be used in order to either set the 518 | // dimensions of the node if none already exist, or to compute the 519 | // remaining space left for the flexible children. 520 | var/*float*/ mainContentDim = 0; 521 | 522 | // There are three kind of children, non flexible, flexible and absolute. 523 | // We need to know how many there are in order to distribute the space. 524 | var/*int*/ flexibleChildrenCount = 0; 525 | var/*float*/ totalFlexible = 0; 526 | var/*int*/ nonFlexibleChildrenCount = 0; 527 | 528 | var/*float*/ maxWidth; 529 | for (i = startLine; i < node.children.length; ++i) { 530 | child = node.children[i]; 531 | var/*float*/ nextContentDim = 0; 532 | 533 | // It only makes sense to consider a child flexible if we have a computed 534 | // dimension for the node. 535 | if (!isUndefined(node.layout[dim[mainAxis]]) && isFlex(child)) { 536 | flexibleChildrenCount++; 537 | totalFlexible += getFlex(child); 538 | 539 | // Even if we don't know its exact size yet, we already know the padding, 540 | // border and margin. We'll use this partial information, which represents 541 | // the smallest possible size for the child, to compute the remaining 542 | // available space. 543 | nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + 544 | getMarginAxis(child, mainAxis); 545 | 546 | } else { 547 | maxWidth = CSS_UNDEFINED; 548 | if (!isRowDirection(mainAxis)) { 549 | maxWidth = parentMaxWidth - 550 | getMarginAxis(node, resolvedRowAxis) - 551 | getPaddingAndBorderAxis(node, resolvedRowAxis); 552 | 553 | if (isDimDefined(node, resolvedRowAxis)) { 554 | maxWidth = node.layout[dim[resolvedRowAxis]] - 555 | getPaddingAndBorderAxis(node, resolvedRowAxis); 556 | } 557 | } 558 | 559 | // This is the main recursive call. We layout non flexible children. 560 | if (alreadyComputedNextLayout === 0) { 561 | layoutNode(/*(java)!layoutContext, */child, maxWidth, direction); 562 | } 563 | 564 | // Absolute positioned elements do not take part of the layout, so we 565 | // don't use them to compute mainContentDim 566 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 567 | nonFlexibleChildrenCount++; 568 | // At this point we know the final size and margin of the element. 569 | nextContentDim = getDimWithMargin(child, mainAxis); 570 | } 571 | } 572 | 573 | // The element we are about to add would make us go to the next line 574 | if (isFlexWrap(node) && 575 | !isUndefined(node.layout[dim[mainAxis]]) && 576 | mainContentDim + nextContentDim > definedMainDim && 577 | // If there's only one element, then it's bigger than the content 578 | // and needs its own line 579 | i !== startLine) { 580 | nonFlexibleChildrenCount--; 581 | alreadyComputedNextLayout = 1; 582 | break; 583 | } 584 | alreadyComputedNextLayout = 0; 585 | mainContentDim += nextContentDim; 586 | endLine = i + 1; 587 | } 588 | 589 | // Layout flexible children and allocate empty space 590 | 591 | // In order to position the elements in the main axis, we have two 592 | // controls. The space between the beginning and the first element 593 | // and the space between each two elements. 594 | var/*float*/ leadingMainDim = 0; 595 | var/*float*/ betweenMainDim = 0; 596 | 597 | // The remaining available space that needs to be allocated 598 | var/*float*/ remainingMainDim = 0; 599 | if (!isUndefined(node.layout[dim[mainAxis]])) { 600 | remainingMainDim = definedMainDim - mainContentDim; 601 | } else { 602 | remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim; 603 | } 604 | 605 | // If there are flexible children in the mix, they are going to fill the 606 | // remaining space 607 | if (flexibleChildrenCount !== 0) { 608 | var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible; 609 | var/*float*/ baseMainDim; 610 | var/*float*/ boundMainDim; 611 | 612 | // Iterate over every child in the axis. If the flex share of remaining 613 | // space doesn't meet min/max bounds, remove this child from flex 614 | // calculations. 615 | for (i = startLine; i < endLine; ++i) { 616 | child = node.children[i]; 617 | if (isFlex(child)) { 618 | baseMainDim = flexibleMainDim * getFlex(child) + 619 | getPaddingAndBorderAxis(child, mainAxis); 620 | boundMainDim = boundAxis(child, mainAxis, baseMainDim); 621 | 622 | if (baseMainDim !== boundMainDim) { 623 | remainingMainDim -= boundMainDim; 624 | totalFlexible -= getFlex(child); 625 | } 626 | } 627 | } 628 | flexibleMainDim = remainingMainDim / totalFlexible; 629 | 630 | // The non flexible children can overflow the container, in this case 631 | // we should just assume that there is no space available. 632 | if (flexibleMainDim < 0) { 633 | flexibleMainDim = 0; 634 | } 635 | // We iterate over the full array and only apply the action on flexible 636 | // children. This is faster than actually allocating a new array that 637 | // contains only flexible children. 638 | for (i = startLine; i < endLine; ++i) { 639 | child = node.children[i]; 640 | if (isFlex(child)) { 641 | // At this point we know the final size of the element in the main 642 | // dimension 643 | child.layout[dim[mainAxis]] = boundAxis(child, mainAxis, 644 | flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) 645 | ); 646 | 647 | maxWidth = CSS_UNDEFINED; 648 | if (isDimDefined(node, resolvedRowAxis)) { 649 | maxWidth = node.layout[dim[resolvedRowAxis]] - 650 | getPaddingAndBorderAxis(node, resolvedRowAxis); 651 | } else if (!isRowDirection(mainAxis)) { 652 | maxWidth = parentMaxWidth - 653 | getMarginAxis(node, resolvedRowAxis) - 654 | getPaddingAndBorderAxis(node, resolvedRowAxis); 655 | } 656 | 657 | // And we recursively call the layout algorithm for this child 658 | layoutNode(/*(java)!layoutContext, */child, maxWidth, direction); 659 | } 660 | } 661 | 662 | // We use justifyContent to figure out how to allocate the remaining 663 | // space available 664 | } else { 665 | var/*css_justify_t*/ justifyContent = getJustifyContent(node); 666 | if (justifyContent === CSS_JUSTIFY_CENTER) { 667 | leadingMainDim = remainingMainDim / 2; 668 | } else if (justifyContent === CSS_JUSTIFY_FLEX_END) { 669 | leadingMainDim = remainingMainDim; 670 | } else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) { 671 | remainingMainDim = fmaxf(remainingMainDim, 0); 672 | if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) { 673 | betweenMainDim = remainingMainDim / 674 | (flexibleChildrenCount + nonFlexibleChildrenCount - 1); 675 | } else { 676 | betweenMainDim = 0; 677 | } 678 | } else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) { 679 | // Space on the edges is half of the space between elements 680 | betweenMainDim = remainingMainDim / 681 | (flexibleChildrenCount + nonFlexibleChildrenCount); 682 | leadingMainDim = betweenMainDim / 2; 683 | } 684 | } 685 | 686 | // Position elements in the main axis and compute dimensions 687 | 688 | // At this point, all the children have their dimensions set. We need to 689 | // find their position. In order to do that, we accumulate data in 690 | // variables that are also useful to compute the total dimensions of the 691 | // container! 692 | var/*float*/ crossDim = 0; 693 | var/*float*/ mainDim = leadingMainDim + 694 | getLeadingPaddingAndBorder(node, mainAxis); 695 | 696 | for (i = startLine; i < endLine; ++i) { 697 | child = node.children[i]; 698 | 699 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 700 | isPosDefined(child, leading[mainAxis])) { 701 | // In case the child is position absolute and has left/top being 702 | // defined, we override the position to whatever the user said 703 | // (and margin/border). 704 | child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + 705 | getLeadingBorder(node, mainAxis) + 706 | getLeadingMargin(child, mainAxis); 707 | } else { 708 | // If the child is position absolute (without top/left) or relative, 709 | // we put it at the current accumulated offset. 710 | child.layout[pos[mainAxis]] += mainDim; 711 | 712 | // Define the trailing position accordingly. 713 | if (!isUndefined(node.layout[dim[mainAxis]])) { 714 | setTrailingPosition(node, child, mainAxis); 715 | } 716 | } 717 | 718 | // Now that we placed the element, we need to update the variables 719 | // We only need to do that for relative elements. Absolute elements 720 | // do not take part in that phase. 721 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 722 | // The main dimension is the sum of all the elements dimension plus 723 | // the spacing. 724 | mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); 725 | // The cross dimension is the max of the elements dimension since there 726 | // can only be one element in that cross dimension. 727 | crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); 728 | } 729 | } 730 | 731 | var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]]; 732 | if (isUndefined(node.layout[dim[crossAxis]])) { 733 | containerCrossAxis = fmaxf( 734 | // For the cross dim, we add both sides at the end because the value 735 | // is aggregate via a max function. Intermediate negative values 736 | // can mess this computation otherwise 737 | boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), 738 | getPaddingAndBorderAxis(node, crossAxis) 739 | ); 740 | } 741 | 742 | // Position elements in the cross axis 743 | 744 | for (i = startLine; i < endLine; ++i) { 745 | child = node.children[i]; 746 | 747 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 748 | isPosDefined(child, leading[crossAxis])) { 749 | // In case the child is absolutely positionned and has a 750 | // top/left/bottom/right being set, we override all the previously 751 | // computed positions to set it correctly. 752 | child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + 753 | getLeadingBorder(node, crossAxis) + 754 | getLeadingMargin(child, crossAxis); 755 | 756 | } else { 757 | var/*float*/ leadingCrossDim = getLeadingPaddingAndBorder(node, crossAxis); 758 | 759 | // For a relative children, we're either using alignItems (parent) or 760 | // alignSelf (child) in order to determine the position in the cross axis 761 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 762 | var/*css_align_t*/ alignItem = getAlignItem(node, child); 763 | if (alignItem === CSS_ALIGN_STRETCH) { 764 | // You can only stretch if the dimension has not already been set 765 | // previously. 766 | if (!isDimDefined(child, crossAxis)) { 767 | child.layout[dim[crossAxis]] = fmaxf( 768 | boundAxis(child, crossAxis, containerCrossAxis - 769 | getPaddingAndBorderAxis(node, crossAxis) - 770 | getMarginAxis(child, crossAxis)), 771 | // You never want to go smaller than padding 772 | getPaddingAndBorderAxis(child, crossAxis) 773 | ); 774 | } 775 | } else if (alignItem !== CSS_ALIGN_FLEX_START) { 776 | // The remaining space between the parent dimensions+padding and child 777 | // dimensions+margin. 778 | var/*float*/ remainingCrossDim = containerCrossAxis - 779 | getPaddingAndBorderAxis(node, crossAxis) - 780 | getDimWithMargin(child, crossAxis); 781 | 782 | if (alignItem === CSS_ALIGN_CENTER) { 783 | leadingCrossDim += remainingCrossDim / 2; 784 | } else { // CSS_ALIGN_FLEX_END 785 | leadingCrossDim += remainingCrossDim; 786 | } 787 | } 788 | } 789 | 790 | // And we apply the position 791 | child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim; 792 | 793 | // Define the trailing position accordingly. 794 | if (!isUndefined(node.layout[dim[crossAxis]])) { 795 | setTrailingPosition(node, child, crossAxis); 796 | } 797 | } 798 | } 799 | 800 | linesCrossDim += crossDim; 801 | linesMainDim = fmaxf(linesMainDim, mainDim); 802 | startLine = endLine; 803 | } 804 | 805 | var/*bool*/ needsMainTrailingPos = false; 806 | var/*bool*/ needsCrossTrailingPos = false; 807 | 808 | // If the user didn't specify a width or height, and it has not been set 809 | // by the container, then we set it via the children. 810 | if (isUndefined(node.layout[dim[mainAxis]])) { 811 | node.layout[dim[mainAxis]] = fmaxf( 812 | // We're missing the last padding at this point to get the final 813 | // dimension 814 | boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)), 815 | // We can never assign a width smaller than the padding and borders 816 | getPaddingAndBorderAxis(node, mainAxis) 817 | ); 818 | 819 | needsMainTrailingPos = true; 820 | } 821 | 822 | if (isUndefined(node.layout[dim[crossAxis]])) { 823 | node.layout[dim[crossAxis]] = fmaxf( 824 | // For the cross dim, we add both sides at the end because the value 825 | // is aggregate via a max function. Intermediate negative values 826 | // can mess this computation otherwise 827 | boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), 828 | getPaddingAndBorderAxis(node, crossAxis) 829 | ); 830 | 831 | needsCrossTrailingPos = true; 832 | } 833 | 834 | // Set trailing position if necessary 835 | 836 | if (needsMainTrailingPos || needsCrossTrailingPos) { 837 | for (i = 0; i < node.children.length; ++i) { 838 | child = node.children[i]; 839 | 840 | if (needsMainTrailingPos) { 841 | setTrailingPosition(node, child, mainAxis); 842 | } 843 | 844 | if (needsCrossTrailingPos) { 845 | setTrailingPosition(node, child, crossAxis); 846 | } 847 | } 848 | } 849 | 850 | // Calculate dimensions for absolutely positioned elements 851 | 852 | for (i = 0; i < node.children.length; ++i) { 853 | child = node.children[i]; 854 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 855 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 856 | // left and right or top and bottom). 857 | for (ii = 0; ii < 2; ii++) { 858 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 859 | if (!isUndefined(node.layout[dim[axis]]) && 860 | !isDimDefined(child, axis) && 861 | isPosDefined(child, leading[axis]) && 862 | isPosDefined(child, trailing[axis])) { 863 | child.layout[dim[axis]] = fmaxf( 864 | boundAxis(child, axis, node.layout[dim[axis]] - 865 | getBorderAxis(node, axis) - 866 | getMarginAxis(child, axis) - 867 | getPosition(child, leading[axis]) - 868 | getPosition(child, trailing[axis]) 869 | ), 870 | // You never want to go smaller than padding 871 | getPaddingAndBorderAxis(child, axis) 872 | ); 873 | } 874 | } 875 | for (ii = 0; ii < 2; ii++) { 876 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 877 | if (isPosDefined(child, trailing[axis]) && 878 | !isPosDefined(child, leading[axis])) { 879 | child.layout[leading[axis]] = 880 | node.layout[dim[axis]] - 881 | child.layout[dim[axis]] - 882 | getPosition(child, trailing[axis]); 883 | } 884 | } 885 | } 886 | } 887 | } 888 | 889 | return { 890 | computeLayout: layoutNode, 891 | fillNodes: fillNodes, 892 | extractNodes: extractNodes 893 | }; 894 | })(); 895 | 896 | // UMD (Universal Module Definition) 897 | // See https://github.com/umdjs/umd for reference 898 | (function (root, factory) { 899 | if (typeof define === 'function' && define.amd) { 900 | // AMD. Register as an anonymous module. 901 | define([], factory); 902 | } else if (typeof exports === 'object') { 903 | // Node. Does not work with strict CommonJS, but 904 | // only CommonJS-like environments that support module.exports, 905 | // like Node. 906 | module.exports = factory(); 907 | } else { 908 | // Browser globals (root is window) 909 | root.returnExports = factory(); 910 | } 911 | }(this, function () { 912 | return computeLayout; 913 | })); 914 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/lib/css-layout/Layout2.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 108 | layout.children = node.children.map(extractNodes); 109 | } else { 110 | delete node.children; 111 | } 112 | 113 | delete layout.right; 114 | delete layout.bottom; 115 | 116 | return layout; 117 | } 118 | 119 | function getPositiveSpacing(node, type, suffix, locations) { 120 | for (var i = 0; i < locations.length; ++i) { 121 | var location = locations[i]; 122 | 123 | var key = type + capitalizeFirst(location) + suffix; 124 | if (key in node.style && node.style[key] >= 0) { 125 | return node.style[key]; 126 | } 127 | 128 | key = type + suffix; 129 | if (key in node.style && node.style[key] >= 0) { 130 | return node.style[key]; 131 | } 132 | } 133 | 134 | return 0; 135 | } 136 | 137 | function isUndefined(value) { 138 | return value === undefined; 139 | } 140 | 141 | function isRowDirection(flexDirection) { 142 | return flexDirection === CSS_FLEX_DIRECTION_ROW || 143 | flexDirection === CSS_FLEX_DIRECTION_ROW_REVERSE; 144 | } 145 | 146 | function isColumnDirection(flexDirection) { 147 | return flexDirection === CSS_FLEX_DIRECTION_COLUMN || 148 | flexDirection === CSS_FLEX_DIRECTION_COLUMN_REVERSE; 149 | } 150 | 151 | function getLeadingLocations(axis) { 152 | var locations = [leading[axis]]; 153 | if (isRowDirection(axis)) { 154 | locations.unshift('start'); 155 | } 156 | 157 | return locations; 158 | } 159 | 160 | function getTrailingLocations(axis) { 161 | var locations = [trailing[axis]]; 162 | if (isRowDirection(axis)) { 163 | locations.unshift('end'); 164 | } 165 | 166 | return locations; 167 | } 168 | 169 | function getMargin(node, locations) { 170 | return getSpacing(node, 'margin', '', locations); 171 | } 172 | 173 | function getLeadingMargin(node, axis) { 174 | return getMargin(node, getLeadingLocations(axis)); 175 | } 176 | 177 | function getTrailingMargin(node, axis) { 178 | return getMargin(node, getTrailingLocations(axis)); 179 | } 180 | 181 | function getPadding(node, locations) { 182 | return getPositiveSpacing(node, 'padding', '', locations); 183 | } 184 | 185 | function getLeadingPadding(node, axis) { 186 | return getPadding(node, getLeadingLocations(axis)); 187 | } 188 | 189 | function getTrailingPadding(node, axis) { 190 | return getPadding(node, getTrailingLocations(axis)); 191 | } 192 | 193 | function getBorder(node, locations) { 194 | return getPositiveSpacing(node, 'border', 'Width', locations); 195 | } 196 | 197 | function getLeadingBorder(node, axis) { 198 | return getBorder(node, getLeadingLocations(axis)); 199 | } 200 | 201 | function getTrailingBorder(node, axis) { 202 | return getBorder(node, getTrailingLocations(axis)); 203 | } 204 | 205 | function getLeadingPaddingAndBorder(node, axis) { 206 | return getLeadingPadding(node, axis) + getLeadingBorder(node, axis); 207 | } 208 | 209 | function getTrailingPaddingAndBorder(node, axis) { 210 | return getTrailingPadding(node, axis) + getTrailingBorder(node, axis); 211 | } 212 | 213 | function getBorderAxis(node, axis) { 214 | return getLeadingBorder(node, axis) + getTrailingBorder(node, axis); 215 | } 216 | 217 | function getMarginAxis(node, axis) { 218 | return getLeadingMargin(node, axis) + getTrailingMargin(node, axis); 219 | } 220 | 221 | function getPaddingAndBorderAxis(node, axis) { 222 | return getLeadingPaddingAndBorder(node, axis) + 223 | getTrailingPaddingAndBorder(node, axis); 224 | } 225 | 226 | function getJustifyContent(node) { 227 | if ('justifyContent' in node.style) { 228 | return node.style.justifyContent; 229 | } 230 | return 'flex-start'; 231 | } 232 | 233 | function getAlignItem(node, child) { 234 | if ('alignSelf' in child.style) { 235 | return child.style.alignSelf; 236 | } 237 | if ('alignItems' in node.style) { 238 | return node.style.alignItems; 239 | } 240 | return 'stretch'; 241 | } 242 | 243 | function resolveAxis(axis, direction) { 244 | if (direction === CSS_DIRECTION_RTL) { 245 | if (axis === CSS_FLEX_DIRECTION_ROW) { 246 | return CSS_FLEX_DIRECTION_ROW_REVERSE; 247 | } else if (axis === CSS_FLEX_DIRECTION_ROW_REVERSE) { 248 | return CSS_FLEX_DIRECTION_ROW; 249 | } 250 | } 251 | 252 | return axis; 253 | } 254 | 255 | function resolveDirection(node, parentDirection) { 256 | var direction; 257 | if ('direction' in node.style) { 258 | direction = node.style.direction; 259 | } else { 260 | direction = CSS_DIRECTION_INHERIT; 261 | } 262 | 263 | if (direction === CSS_DIRECTION_INHERIT) { 264 | direction = (parentDirection === undefined ? CSS_DIRECTION_LTR : parentDirection); 265 | } 266 | 267 | return direction; 268 | } 269 | 270 | function getFlexDirection(node) { 271 | if ('flexDirection' in node.style) { 272 | return node.style.flexDirection; 273 | } 274 | return CSS_FLEX_DIRECTION_COLUMN; 275 | } 276 | 277 | function getCrossFlexDirection(flexDirection, direction) { 278 | if (isColumnDirection(flexDirection)) { 279 | return resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); 280 | } else { 281 | return CSS_FLEX_DIRECTION_COLUMN; 282 | } 283 | } 284 | 285 | function getPositionType(node) { 286 | if ('position' in node.style) { 287 | return node.style.position; 288 | } 289 | return 'relative'; 290 | } 291 | 292 | function getFlex(node) { 293 | return node.style.flex; 294 | } 295 | 296 | function isFlex(node) { 297 | return ( 298 | getPositionType(node) === CSS_POSITION_RELATIVE && 299 | getFlex(node) > 0 300 | ); 301 | } 302 | 303 | function isFlexWrap(node) { 304 | return node.style.flexWrap === 'wrap'; 305 | } 306 | 307 | function getDimWithMargin(node, axis) { 308 | return node.layout[dim[axis]] + getMarginAxis(node, axis); 309 | } 310 | 311 | function isDimDefined(node, axis) { 312 | return !isUndefined(node.style[dim[axis]]) && node.style[dim[axis]] >= 0; 313 | } 314 | 315 | function isPosDefined(node, pos) { 316 | return !isUndefined(node.style[pos]); 317 | } 318 | 319 | function isMeasureDefined(node) { 320 | return 'measure' in node.style; 321 | } 322 | 323 | function getPosition(node, pos) { 324 | if (pos in node.style) { 325 | return node.style[pos]; 326 | } 327 | return 0; 328 | } 329 | 330 | function boundAxis(node, axis, value) { 331 | var min = { 332 | 'row': node.style.minWidth, 333 | 'row-reverse': node.style.minWidth, 334 | 'column': node.style.minHeight, 335 | 'column-reverse': node.style.minHeight 336 | }[axis]; 337 | 338 | var max = { 339 | 'row': node.style.maxWidth, 340 | 'row-reverse': node.style.maxWidth, 341 | 'column': node.style.maxHeight, 342 | 'column-reverse': node.style.maxHeight 343 | }[axis]; 344 | 345 | var boundValue = value; 346 | if (!isUndefined(max) && max >= 0 && boundValue > max) { 347 | boundValue = max; 348 | } 349 | if (!isUndefined(min) && min >= 0 && boundValue < min) { 350 | boundValue = min; 351 | } 352 | return boundValue; 353 | } 354 | 355 | function fmaxf(a, b) { 356 | if (a > b) { 357 | return a; 358 | } 359 | return b; 360 | } 361 | 362 | // When the user specifically sets a value for width or height 363 | function setDimensionFromStyle(node, axis) { 364 | // The parent already computed us a width or height. We just skip it 365 | if (!isUndefined(node.layout[dim[axis]])) { 366 | return; 367 | } 368 | // We only run if there's a width or height defined 369 | if (!isDimDefined(node, axis)) { 370 | return; 371 | } 372 | 373 | // The dimensions can never be smaller than the padding and border 374 | node.layout[dim[axis]] = fmaxf( 375 | boundAxis(node, axis, node.style[dim[axis]]), 376 | getPaddingAndBorderAxis(node, axis) 377 | ); 378 | } 379 | 380 | function setTrailingPosition(node, child, axis) { 381 | child.layout[trailing[axis]] = node.layout[dim[axis]] - 382 | child.layout[dim[axis]] - child.layout[pos[axis]]; 383 | } 384 | 385 | // If both left and right are defined, then use left. Otherwise return 386 | // +left or -right depending on which is defined. 387 | function getRelativePosition(node, axis) { 388 | if (leading[axis] in node.style) { 389 | return getPosition(node, leading[axis]); 390 | } 391 | return -getPosition(node, trailing[axis]); 392 | } 393 | 394 | function layoutNode(node, parentMaxWidth, /*css_direction_t*/parentDirection) { 395 | var/*css_direction_t*/ direction = resolveDirection(node, parentDirection); 396 | var/*css_flex_direction_t*/ mainAxis = resolveAxis(getFlexDirection(node), direction); 397 | var/*css_flex_direction_t*/ crossAxis = getCrossFlexDirection(mainAxis, direction); 398 | var/*css_flex_direction_t*/ resolvedRowAxis = resolveAxis(CSS_FLEX_DIRECTION_ROW, direction); 399 | 400 | // Handle width and height style attributes 401 | setDimensionFromStyle(node, mainAxis); 402 | setDimensionFromStyle(node, crossAxis); 403 | 404 | // The position is set by the parent, but we need to complete it with a 405 | // delta composed of the margin and left/top/right/bottom 406 | node.layout[leading[mainAxis]] += getLeadingMargin(node, mainAxis) + 407 | getRelativePosition(node, mainAxis); 408 | node.layout[trailing[mainAxis]] += getTrailingMargin(node, mainAxis) + 409 | getRelativePosition(node, mainAxis); 410 | node.layout[leading[crossAxis]] += getLeadingMargin(node, crossAxis) + 411 | getRelativePosition(node, crossAxis); 412 | node.layout[trailing[crossAxis]] += getTrailingMargin(node, crossAxis) + 413 | getRelativePosition(node, crossAxis); 414 | 415 | if (isMeasureDefined(node)) { 416 | var/*float*/ width = CSS_UNDEFINED; 417 | if (isDimDefined(node, resolvedRowAxis)) { 418 | width = node.style.width; 419 | } else if (!isUndefined(node.layout[dim[resolvedRowAxis]])) { 420 | width = node.layout[dim[resolvedRowAxis]]; 421 | } else { 422 | width = parentMaxWidth - 423 | getMarginAxis(node, resolvedRowAxis); 424 | } 425 | width -= getPaddingAndBorderAxis(node, resolvedRowAxis); 426 | 427 | // We only need to give a dimension for the text if we haven't got any 428 | // for it computed yet. It can either be from the style attribute or because 429 | // the element is flexible. 430 | var/*bool*/ isRowUndefined = !isDimDefined(node, resolvedRowAxis) && 431 | isUndefined(node.layout[dim[resolvedRowAxis]]); 432 | var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && 433 | isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]); 434 | 435 | // Let's not measure the text if we already know both dimensions 436 | if (isRowUndefined || isColumnUndefined) { 437 | var/*css_dim_t*/ measureDim = node.style.measure( 438 | /*(c)!node->context,*/ 439 | /*(java)!layoutContext.measureOutput,*/ 440 | width 441 | ); 442 | if (isRowUndefined) { 443 | node.layout.width = measureDim.width + 444 | getPaddingAndBorderAxis(node, resolvedRowAxis); 445 | } 446 | if (isColumnUndefined) { 447 | node.layout.height = measureDim.height + 448 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); 449 | } 450 | } 451 | if (node.children.length === 0) { 452 | return; 453 | } 454 | } 455 | 456 | var/*int*/ i; 457 | var/*int*/ ii; 458 | var/*css_node_t**/ child; 459 | var/*css_flex_direction_t*/ axis; 460 | 461 | // Pre-fill some dimensions straight from the parent 462 | for (i = 0; i < node.children.length; ++i) { 463 | child = node.children[i]; 464 | // Pre-fill cross axis dimensions when the child is using stretch before 465 | // we call the recursive layout pass 466 | if (getAlignItem(node, child) === CSS_ALIGN_STRETCH && 467 | getPositionType(child) === CSS_POSITION_RELATIVE && 468 | !isUndefined(node.layout[dim[crossAxis]]) && 469 | !isDimDefined(child, crossAxis)) { 470 | child.layout[dim[crossAxis]] = fmaxf( 471 | boundAxis(child, crossAxis, node.layout[dim[crossAxis]] - 472 | getPaddingAndBorderAxis(node, crossAxis) - 473 | getMarginAxis(child, crossAxis)), 474 | // You never want to go smaller than padding 475 | getPaddingAndBorderAxis(child, crossAxis) 476 | ); 477 | } else if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 478 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 479 | // left and right or top and bottom). 480 | for (ii = 0; ii < 2; ii++) { 481 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 482 | if (!isUndefined(node.layout[dim[axis]]) && 483 | !isDimDefined(child, axis) && 484 | isPosDefined(child, leading[axis]) && 485 | isPosDefined(child, trailing[axis])) { 486 | child.layout[dim[axis]] = fmaxf( 487 | boundAxis(child, axis, node.layout[dim[axis]] - 488 | getPaddingAndBorderAxis(node, axis) - 489 | getMarginAxis(child, axis) - 490 | getPosition(child, leading[axis]) - 491 | getPosition(child, trailing[axis])), 492 | // You never want to go smaller than padding 493 | getPaddingAndBorderAxis(child, axis) 494 | ); 495 | } 496 | } 497 | } 498 | } 499 | 500 | var/*float*/ definedMainDim = CSS_UNDEFINED; 501 | if (!isUndefined(node.layout[dim[mainAxis]])) { 502 | definedMainDim = node.layout[dim[mainAxis]] - 503 | getPaddingAndBorderAxis(node, mainAxis); 504 | } 505 | 506 | // We want to execute the next two loops one per line with flex-wrap 507 | var/*int*/ startLine = 0; 508 | var/*int*/ endLine = 0; 509 | // var/*int*/ nextOffset = 0; 510 | var/*int*/ alreadyComputedNextLayout = 0; 511 | // We aggregate the total dimensions of the container in those two variables 512 | var/*float*/ linesCrossDim = 0; 513 | var/*float*/ linesMainDim = 0; 514 | while (endLine < node.children.length) { 515 | // Layout non flexible children and count children by type 516 | 517 | // mainContentDim is accumulation of the dimensions and margin of all the 518 | // non flexible children. This will be used in order to either set the 519 | // dimensions of the node if none already exist, or to compute the 520 | // remaining space left for the flexible children. 521 | var/*float*/ mainContentDim = 0; 522 | 523 | // There are three kind of children, non flexible, flexible and absolute. 524 | // We need to know how many there are in order to distribute the space. 525 | var/*int*/ flexibleChildrenCount = 0; 526 | var/*float*/ totalFlexible = 0; 527 | var/*int*/ nonFlexibleChildrenCount = 0; 528 | 529 | var/*float*/ maxWidth; 530 | for (i = startLine; i < node.children.length; ++i) { 531 | child = node.children[i]; 532 | var/*float*/ nextContentDim = 0; 533 | 534 | // It only makes sense to consider a child flexible if we have a computed 535 | // dimension for the node. 536 | if (!isUndefined(node.layout[dim[mainAxis]]) && isFlex(child)) { 537 | flexibleChildrenCount++; 538 | totalFlexible += getFlex(child); 539 | 540 | // Even if we don't know its exact size yet, we already know the padding, 541 | // border and margin. We'll use this partial information, which represents 542 | // the smallest possible size for the child, to compute the remaining 543 | // available space. 544 | nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + 545 | getMarginAxis(child, mainAxis); 546 | 547 | } else { 548 | maxWidth = CSS_UNDEFINED; 549 | if (!isRowDirection(mainAxis)) { 550 | maxWidth = parentMaxWidth - 551 | getMarginAxis(node, resolvedRowAxis) - 552 | getPaddingAndBorderAxis(node, resolvedRowAxis); 553 | 554 | if (isDimDefined(node, resolvedRowAxis)) { 555 | maxWidth = node.layout[dim[resolvedRowAxis]] - 556 | getPaddingAndBorderAxis(node, resolvedRowAxis); 557 | } 558 | } 559 | 560 | // This is the main recursive call. We layout non flexible children. 561 | if (alreadyComputedNextLayout === 0) { 562 | layoutNode(/*(java)!layoutContext, */child, maxWidth, direction); 563 | } 564 | 565 | // Absolute positioned elements do not take part of the layout, so we 566 | // don't use them to compute mainContentDim 567 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 568 | nonFlexibleChildrenCount++; 569 | // At this point we know the final size and margin of the element. 570 | nextContentDim = getDimWithMargin(child, mainAxis); 571 | } 572 | } 573 | 574 | // The element we are about to add would make us go to the next line 575 | if (isFlexWrap(node) && 576 | !isUndefined(node.layout[dim[mainAxis]]) && 577 | mainContentDim + nextContentDim > definedMainDim && 578 | // If there's only one element, then it's bigger than the content 579 | // and needs its own line 580 | i !== startLine) { 581 | nonFlexibleChildrenCount--; 582 | alreadyComputedNextLayout = 1; 583 | break; 584 | } 585 | alreadyComputedNextLayout = 0; 586 | mainContentDim += nextContentDim; 587 | endLine = i + 1; 588 | } 589 | 590 | // Layout flexible children and allocate empty space 591 | 592 | // In order to position the elements in the main axis, we have two 593 | // controls. The space between the beginning and the first element 594 | // and the space between each two elements. 595 | var/*float*/ leadingMainDim = 0; 596 | var/*float*/ betweenMainDim = 0; 597 | 598 | // The remaining available space that needs to be allocated 599 | var/*float*/ remainingMainDim = 0; 600 | if (!isUndefined(node.layout[dim[mainAxis]])) { 601 | remainingMainDim = definedMainDim - mainContentDim; 602 | } else { 603 | remainingMainDim = fmaxf(mainContentDim, 0) - mainContentDim; 604 | } 605 | 606 | // If there are flexible children in the mix, they are going to fill the 607 | // remaining space 608 | if (flexibleChildrenCount !== 0) { 609 | var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible; 610 | var/*float*/ baseMainDim; 611 | var/*float*/ boundMainDim; 612 | 613 | // Iterate over every child in the axis. If the flex share of remaining 614 | // space doesn't meet min/max bounds, remove this child from flex 615 | // calculations. 616 | for (i = startLine; i < endLine; ++i) { 617 | child = node.children[i]; 618 | if (isFlex(child)) { 619 | baseMainDim = flexibleMainDim * getFlex(child) + 620 | getPaddingAndBorderAxis(child, mainAxis); 621 | boundMainDim = boundAxis(child, mainAxis, baseMainDim); 622 | 623 | if (baseMainDim !== boundMainDim) { 624 | remainingMainDim -= boundMainDim; 625 | totalFlexible -= getFlex(child); 626 | } 627 | } 628 | } 629 | flexibleMainDim = remainingMainDim / totalFlexible; 630 | 631 | // The non flexible children can overflow the container, in this case 632 | // we should just assume that there is no space available. 633 | if (flexibleMainDim < 0) { 634 | flexibleMainDim = 0; 635 | } 636 | // We iterate over the full array and only apply the action on flexible 637 | // children. This is faster than actually allocating a new array that 638 | // contains only flexible children. 639 | for (i = startLine; i < endLine; ++i) { 640 | child = node.children[i]; 641 | if (isFlex(child)) { 642 | // At this point we know the final size of the element in the main 643 | // dimension 644 | child.layout[dim[mainAxis]] = boundAxis(child, mainAxis, 645 | flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) 646 | ); 647 | 648 | maxWidth = CSS_UNDEFINED; 649 | if (isDimDefined(node, resolvedRowAxis)) { 650 | maxWidth = node.layout[dim[resolvedRowAxis]] - 651 | getPaddingAndBorderAxis(node, resolvedRowAxis); 652 | } else if (!isRowDirection(mainAxis)) { 653 | maxWidth = parentMaxWidth - 654 | getMarginAxis(node, resolvedRowAxis) - 655 | getPaddingAndBorderAxis(node, resolvedRowAxis); 656 | } 657 | 658 | // And we recursively call the layout algorithm for this child 659 | layoutNode(/*(java)!layoutContext, */child, maxWidth, direction); 660 | } 661 | } 662 | 663 | // We use justifyContent to figure out how to allocate the remaining 664 | // space available 665 | } else { 666 | var/*css_justify_t*/ justifyContent = getJustifyContent(node); 667 | if (justifyContent === CSS_JUSTIFY_CENTER) { 668 | leadingMainDim = remainingMainDim / 2; 669 | } else if (justifyContent === CSS_JUSTIFY_FLEX_END) { 670 | leadingMainDim = remainingMainDim; 671 | } else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) { 672 | remainingMainDim = fmaxf(remainingMainDim, 0); 673 | if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) { 674 | betweenMainDim = remainingMainDim / 675 | (flexibleChildrenCount + nonFlexibleChildrenCount - 1); 676 | } else { 677 | betweenMainDim = 0; 678 | } 679 | } else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) { 680 | // Space on the edges is half of the space between elements 681 | betweenMainDim = remainingMainDim / 682 | (flexibleChildrenCount + nonFlexibleChildrenCount); 683 | leadingMainDim = betweenMainDim / 2; 684 | } 685 | } 686 | 687 | // Position elements in the main axis and compute dimensions 688 | 689 | // At this point, all the children have their dimensions set. We need to 690 | // find their position. In order to do that, we accumulate data in 691 | // variables that are also useful to compute the total dimensions of the 692 | // container! 693 | var/*float*/ crossDim = 0; 694 | var/*float*/ mainDim = leadingMainDim + 695 | getLeadingPaddingAndBorder(node, mainAxis); 696 | 697 | for (i = startLine; i < endLine; ++i) { 698 | child = node.children[i]; 699 | 700 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 701 | isPosDefined(child, leading[mainAxis])) { 702 | // In case the child is position absolute and has left/top being 703 | // defined, we override the position to whatever the user said 704 | // (and margin/border). 705 | child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + 706 | getLeadingBorder(node, mainAxis) + 707 | getLeadingMargin(child, mainAxis); 708 | } else { 709 | // If the child is position absolute (without top/left) or relative, 710 | // we put it at the current accumulated offset. 711 | child.layout[pos[mainAxis]] += mainDim; 712 | 713 | // Define the trailing position accordingly. 714 | if (!isUndefined(node.layout[dim[mainAxis]])) { 715 | setTrailingPosition(node, child, mainAxis); 716 | } 717 | } 718 | 719 | // Now that we placed the element, we need to update the variables 720 | // We only need to do that for relative elements. Absolute elements 721 | // do not take part in that phase. 722 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 723 | // The main dimension is the sum of all the elements dimension plus 724 | // the spacing. 725 | mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); 726 | // The cross dimension is the max of the elements dimension since there 727 | // can only be one element in that cross dimension. 728 | crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); 729 | } 730 | } 731 | 732 | var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]]; 733 | if (isUndefined(node.layout[dim[crossAxis]])) { 734 | containerCrossAxis = fmaxf( 735 | // For the cross dim, we add both sides at the end because the value 736 | // is aggregate via a max function. Intermediate negative values 737 | // can mess this computation otherwise 738 | boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), 739 | getPaddingAndBorderAxis(node, crossAxis) 740 | ); 741 | } 742 | 743 | // Position elements in the cross axis 744 | 745 | for (i = startLine; i < endLine; ++i) { 746 | child = node.children[i]; 747 | 748 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 749 | isPosDefined(child, leading[crossAxis])) { 750 | // In case the child is absolutely positionned and has a 751 | // top/left/bottom/right being set, we override all the previously 752 | // computed positions to set it correctly. 753 | child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + 754 | getLeadingBorder(node, crossAxis) + 755 | getLeadingMargin(child, crossAxis); 756 | 757 | } else { 758 | var/*float*/ leadingCrossDim = getLeadingPaddingAndBorder(node, crossAxis); 759 | 760 | // For a relative children, we're either using alignItems (parent) or 761 | // alignSelf (child) in order to determine the position in the cross axis 762 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 763 | var/*css_align_t*/ alignItem = getAlignItem(node, child); 764 | if (alignItem === CSS_ALIGN_STRETCH) { 765 | // You can only stretch if the dimension has not already been set 766 | // previously. 767 | if (!isDimDefined(child, crossAxis)) { 768 | child.layout[dim[crossAxis]] = fmaxf( 769 | boundAxis(child, crossAxis, containerCrossAxis - 770 | getPaddingAndBorderAxis(node, crossAxis) - 771 | getMarginAxis(child, crossAxis)), 772 | // You never want to go smaller than padding 773 | getPaddingAndBorderAxis(child, crossAxis) 774 | ); 775 | } 776 | } else if (alignItem !== CSS_ALIGN_FLEX_START) { 777 | // The remaining space between the parent dimensions+padding and child 778 | // dimensions+margin. 779 | var/*float*/ remainingCrossDim = containerCrossAxis - 780 | getPaddingAndBorderAxis(node, crossAxis) - 781 | getDimWithMargin(child, crossAxis); 782 | 783 | if (alignItem === CSS_ALIGN_CENTER) { 784 | leadingCrossDim += remainingCrossDim / 2; 785 | } else { // CSS_ALIGN_FLEX_END 786 | leadingCrossDim += remainingCrossDim; 787 | } 788 | } 789 | } 790 | 791 | // And we apply the position 792 | child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim; 793 | 794 | // Define the trailing position accordingly. 795 | if (!isUndefined(node.layout[dim[crossAxis]])) { 796 | setTrailingPosition(node, child, crossAxis); 797 | } 798 | } 799 | } 800 | 801 | linesCrossDim += crossDim; 802 | linesMainDim = fmaxf(linesMainDim, mainDim); 803 | startLine = endLine; 804 | } 805 | 806 | var/*bool*/ needsMainTrailingPos = false; 807 | var/*bool*/ needsCrossTrailingPos = false; 808 | 809 | // If the user didn't specify a width or height, and it has not been set 810 | // by the container, then we set it via the children. 811 | if (isUndefined(node.layout[dim[mainAxis]])) { 812 | node.layout[dim[mainAxis]] = fmaxf( 813 | // We're missing the last padding at this point to get the final 814 | // dimension 815 | boundAxis(node, mainAxis, linesMainDim + getTrailingPaddingAndBorder(node, mainAxis)), 816 | // We can never assign a width smaller than the padding and borders 817 | getPaddingAndBorderAxis(node, mainAxis) 818 | ); 819 | 820 | needsMainTrailingPos = true; 821 | } 822 | 823 | if (isUndefined(node.layout[dim[crossAxis]])) { 824 | node.layout[dim[crossAxis]] = fmaxf( 825 | // For the cross dim, we add both sides at the end because the value 826 | // is aggregate via a max function. Intermediate negative values 827 | // can mess this computation otherwise 828 | boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), 829 | getPaddingAndBorderAxis(node, crossAxis) 830 | ); 831 | 832 | needsCrossTrailingPos = true; 833 | } 834 | 835 | // Set trailing position if necessary 836 | 837 | if (needsMainTrailingPos || needsCrossTrailingPos) { 838 | for (i = 0; i < node.children.length; ++i) { 839 | child = node.children[i]; 840 | 841 | if (needsMainTrailingPos) { 842 | setTrailingPosition(node, child, mainAxis); 843 | } 844 | 845 | if (needsCrossTrailingPos) { 846 | setTrailingPosition(node, child, crossAxis); 847 | } 848 | } 849 | } 850 | 851 | // Calculate dimensions for absolutely positioned elements 852 | 853 | for (i = 0; i < node.children.length; ++i) { 854 | child = node.children[i]; 855 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 856 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 857 | // left and right or top and bottom). 858 | for (ii = 0; ii < 2; ii++) { 859 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 860 | if (!isUndefined(node.layout[dim[axis]]) && 861 | !isDimDefined(child, axis) && 862 | isPosDefined(child, leading[axis]) && 863 | isPosDefined(child, trailing[axis])) { 864 | child.layout[dim[axis]] = fmaxf( 865 | boundAxis(child, axis, node.layout[dim[axis]] - 866 | getBorderAxis(node, axis) - 867 | getMarginAxis(child, axis) - 868 | getPosition(child, leading[axis]) - 869 | getPosition(child, trailing[axis]) 870 | ), 871 | // You never want to go smaller than padding 872 | getPaddingAndBorderAxis(child, axis) 873 | ); 874 | } 875 | } 876 | for (ii = 0; ii < 2; ii++) { 877 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 878 | if (isPosDefined(child, trailing[axis]) && 879 | !isPosDefined(child, leading[axis])) { 880 | child.layout[leading[axis]] = 881 | node.layout[dim[axis]] - 882 | child.layout[dim[axis]] - 883 | getPosition(child, trailing[axis]); 884 | } 885 | } 886 | } 887 | } 888 | } 889 | 890 | return { 891 | computeLayout: layoutNode, 892 | fillNodes: fillNodes, 893 | extractNodes: extractNodes 894 | }; 895 | })(); 896 | 897 | // UMD (Universal Module Definition) 898 | // See https://github.com/umdjs/umd for reference 899 | (function (root, factory) { 900 | if (typeof define === 'function' && define.amd) { 901 | // AMD. Register as an anonymous module. 902 | define([], factory); 903 | } else if (typeof exports === 'object') { 904 | // Node. Does not work with strict CommonJS, but 905 | // only CommonJS-like environments that support module.exports, 906 | // like Node. 907 | module.exports = factory(); 908 | } else { 909 | // Browser globals (root is window) 910 | root.returnExports = factory(); 911 | } 912 | }(this, function () { 913 | return computeLayout; 914 | })); 915 | 916 | },{}]},{},[1]); 917 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/Layout/utils.js: -------------------------------------------------------------------------------- 1 | // global sketch variables setup and init 2 | var app = [NSApplication sharedApplication], 3 | doc, page, plugin, pluginPath, pluginURL, pluginCommand, selection, artboard; 4 | 5 | // debug for profiling and logging 6 | var debug = debug || {}; 7 | debug = { 8 | on : false, 9 | startTime: 0, 10 | partialTime: 0, 11 | // start the profiling 12 | start : function(){ 13 | debug.on = true; 14 | startTime = Date.now(); 15 | partialTime = Date.now(); 16 | log("======= DEBUG START ========="); 17 | }, 18 | // log with current time elapsed from start 19 | log : function(message){ 20 | if (!debug.on) { 21 | return; 22 | } else { 23 | log("==" + (Date.now() - startTime) + "ms== " + message); 24 | } 25 | }, 26 | // log with partial time elapsed and resets partial time 27 | logPart : function(message){ 28 | if (!debug.on) { 29 | return; 30 | } else { 31 | log("--" +(Date.now() - partialTime) + "ms-- " + message); 32 | partialTime = Date.now(); 33 | } 34 | }, 35 | // end debug, log total time 36 | end : function(){ 37 | if (!debug.on) { 38 | return; 39 | } else { 40 | log("======= DEBUG END: " + (Date.now() - startTime) + "ms ========="); 41 | } 42 | } 43 | } 44 | 45 | // namespace utils and initialize with global vars init 46 | var utils = utils || {}; 47 | 48 | utils.init = function(context){ 49 | doc = context.document; 50 | page = doc.currentPage(); 51 | pages = [doc pages]; 52 | selection = context.selection; 53 | artboard = [[doc currentPage] currentArtboard]; 54 | plugin = context.plugin; 55 | pluginPath = [plugin url] + ""; 56 | pluginURL = context.scriptURL; 57 | pluginCommand = context.command; 58 | }; 59 | 60 | // call a function on multiple layers at once 61 | utils.call = { 62 | // call a function recursively on child layers of layer, including the layer. 63 | childLayers : function(layer, callback){ 64 | callback(layer); 65 | if (utils.is.group(layer)) { 66 | var childLayers = [layer layers]; 67 | if (childLayers) { 68 | for (var i = 0; i < childLayers.count(); i++) { 69 | utils.call.childLayers(childLayers[i], callback); 70 | }; 71 | }; 72 | }; 73 | }, 74 | // call a function on all layers on a page recursively, including the page layer. 75 | pageLayers : function(callback){ 76 | utils.call.childLayers(page, callback); 77 | }, 78 | // call a function on selected layers 79 | selectedLayers : function(callback){ 80 | var selectionCount = selection.count(); 81 | if (selectionCount > 0) { 82 | for (var i = 0; i < selectionCount; i++) { 83 | callback(selection[i]); 84 | } 85 | } else { 86 | log("selection is empty"); 87 | } 88 | }, 89 | } 90 | 91 | // layer boolean checks 92 | utils.is = { 93 | // returns whether a layer is a group 94 | group : function(layer){ 95 | if ([layer isKindOfClass:MSLayerGroup] && ![layer isMemberOfClass:MSShapeGroup]) { 96 | return true; 97 | } 98 | return false; 99 | }, 100 | page : function(layer){ 101 | return ([layer isMemberOfClass:MSPage]); 102 | }, 103 | // returns whether a layer is a text layer 104 | textLayer : function(layer){ 105 | return ([layer isMemberOfClass:MSTextLayer]); 106 | }, 107 | // returns whether nothing is selected 108 | selectionEmpty : function(){ 109 | return (selection.count() == 0); 110 | } 111 | } 112 | 113 | // common low-level utils 114 | utils.common = { 115 | // returns whether a string ends with a suffix 116 | endsWithString : function(str, suffix){ 117 | return [str hasSuffix:suffix]; 118 | }, 119 | // returns whether a string starts with a prefix 120 | startsWithString : function(str, prefix){ 121 | return [str hasPrefix:prefix]; 122 | }, 123 | // returns javascript object size 124 | objectSize : function(obj){ 125 | return Object.keys(obj).length; 126 | }, 127 | // flatten a multidimensional js array 128 | flattenArray : function(arr) { 129 | var r = []; 130 | function arrayEqual(a, b) { 131 | var i = Math.max(a.length, b.length, 1); 132 | while(i-- >= 0 && a[i] === b[i]); 133 | return (i === -2); 134 | } 135 | while (!arrayEqual(r, arr)) { 136 | r = arr; 137 | arr = [].concat.apply([], arr); 138 | } 139 | return arr; 140 | }, 141 | //given an array of objects and a key, returns an object with the key value as properties 142 | keyedObjectFromArray : function(array, key){ 143 | var keyedObject = {}; 144 | for (var i = 0; i < array.length; i++) { 145 | var arrayMember = array[i]; 146 | var arrayKey = arrayMember[key]; 147 | delete arrayMember[key]; 148 | keyedObject[arrayKey] = arrayMember; 149 | } 150 | return keyedObject; 151 | }, 152 | // given an object where children ha 153 | arrayOfValuesByKey : function(arr, key){ 154 | var returnArray = []; 155 | for (var i = 0; i < arr.length; i++) { 156 | returnArray.push(arr[i][key]); 157 | } 158 | return returnArray; 159 | } 160 | } 161 | 162 | // miscellaneous layer manipulation and such 163 | utils.misc = { 164 | // moves provided layer into the current selection 165 | moveLayerToSelection : function(layer){ 166 | var selectedLayer = selection[0]; 167 | if (utils.is.group(selectedLayer)) { 168 | [layer removeFromParent]; 169 | [selectedLayer addLayers:[layer]]; 170 | } 171 | } 172 | }; 173 | 174 | // interaction with a separate javascript context 175 | utils.js = { 176 | loadLibrary : function(path){ 177 | var scriptFolder = [pluginURL URLByDeletingLastPathComponent]; 178 | var libraryURL = [scriptFolder URLByAppendingPathComponent:path]; 179 | var fileString = NSString.stringWithContentsOfFile(libraryURL); 180 | return fileString; 181 | } 182 | }; 183 | 184 | // ------------ UI --------- // 185 | 186 | // UI 187 | utils.UI = { 188 | showInput : function (message, initialValue){ 189 | var initial = initialValue || ""; 190 | return [doc askForUserInput:message initialValue:initial]; 191 | }, 192 | showMessage : function(message){ 193 | [doc showMessage:message]; 194 | }, 195 | showError : function(message){ 196 | utils.UI.showDialog("Error", message); 197 | }, 198 | showDialog : function(title, message){ 199 | var app = [NSApplication sharedApplication]; 200 | [app displayDialog:message withTitle:title]; 201 | }, 202 | showSelect : function(msg, items, selectedItemIndex){ 203 | var selectedItemIndex = selectedItemIndex || 0; 204 | 205 | var accessory = [[NSComboBox alloc] initWithFrame:NSMakeRect(0,0,200,25)] 206 | [accessory addItemsWithObjectValues:items] 207 | [accessory selectItemAtIndex:selectedItemIndex] 208 | 209 | var alert = [[NSAlert alloc] init] 210 | [alert setMessageText:msg] 211 | [alert addButtonWithTitle:'OK'] 212 | [alert addButtonWithTitle:'Cancel'] 213 | [alert setAccessoryView:accessory] 214 | 215 | var responseCode = [alert runModal] 216 | var sel = [accessory indexOfSelectedItem] 217 | 218 | return [responseCode, sel] 219 | } 220 | }; 221 | -------------------------------------------------------------------------------- /Flex-Layout.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sketch Flex Layout", 3 | "description" : "CSS Flexbox layout inside of Sketch", 4 | "author" : "Matej Hrescak", 5 | "authorEmail" : "matej@hrescak.com", 6 | "homepage": "https://github.com/hrescak/Sketch-Flex-Layout", 7 | "version" : "1.0", 8 | "identifier" : "com.hrescak.flexlayout", 9 | "compatibleVersion": "3.3", 10 | "bundleVersion": "1", 11 | "commands" : [ 12 | { 13 | "name" : "Compute Layout", 14 | "script" : "Layout/layout.js", 15 | "shortcut" : "cmd ctrl l", 16 | "identifier" : "layoutElements" 17 | }, 18 | { 19 | "name" : "Add Object from Prototype", 20 | "script" : "Layout/layout.js", 21 | "handler" : "newObjectFromPrototype", 22 | "identifier" : "newObjectFromPrototype" 23 | } 24 | ], 25 | "menu" : { 26 | "items" :[ 27 | "layoutElements", 28 | "newObjectFromPrototype" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sketch Flex Layout 2 | A Plugin for Sketch allowing for CSS Flexbox layouts using stylesheets and prototypes. Here's a [Medium article](https://medium.com/@hrescak/exploring-dynamic-layout-in-sketch-fdf0e825d1cf) with some background. To install the plugin download this repository as a ZIP and double click the Flex-Layout.sketchplugin file. **Important note :** *This is a very work in progress version for early testing. A lot of things are subject to change and using it on critical projects is not yet recommended.* 3 | 4 | ![](http://i.imgur.com/Z5A8Hqo.png) 5 | 6 | Flex Layout allows you to use both a stylesheet text layer and 'prototypes'. Prototypes are layer groups with added style layers - their sizes work as base for establishing paddings, margins, sizes etc. There's an [example file](https://github.com/hrescak/Sketch-Flex-Layout/raw/master/ExampleFile.sketch) included in the repository that shows examples of working with both. 7 | 8 | ## Working with stylesheets 9 | 10 | ![](http://i.imgur.com/2FcoADp.png) 11 | 12 | 1. Create a text layer and name it **“@stylesheet”**. 13 | 2. Write css in the layer. Some rules: 14 | - the supported properties are listed [here](https://github.com/facebook/css-layout). 15 | - they are in camelCase not hyphen-ated 16 | - they have no units 17 | - shortcut rules are not supported (yet) 18 | - there are only classes *(.something)* 19 | - so no nested styles *(“\>” declarations)* 20 | 3. Create some layers and append the selectors to them. So if your selector is '.someclass{width:200;}', you rename the layer from 'Rect1' to 'Rect1 .someclass' 21 | 4. Run cmd + ctrl + L for the layout to apply _(make sure your stylesheet layer is de-selected, or the changes will not apply)_ 22 | 23 | ## Working with prototypes 24 | 25 | ![](http://i.imgur.com/Y86vIYJ.png) 26 | 27 | 1. Create a layer group, name it **"prototype .SOMETHING"** 28 | 2. Add rectangles to the group that will define its style - [these are the supported names and dimensions](http://i.imgur.com/IguIeFI.png) 29 | - if you need it, add a text layer named **"@styles"** with layout styles, separated by semicolon - [these are the styles and values](http://i.imgur.com/oseZ1Dr.png) 30 | 3. You can add more groups with their own styles to the prototype group, and these don't need the "prototype" in their name, just the **".somethingelse"** class name 31 | 4. Run _Add Object From Prototype_ action - this will duplicate the prototype, remove all the style layers and if you have a group selected, it will move it under the group. This will also apply the layout. 32 | 5. After you make changes, Run cmd + ctrl + L for the layout to apply. 33 | 34 | **Tip** - you can have both prototypes and a *@stylesheet* layer on the same page. 35 | 36 | **Pro Tip** - when you duplicate your groups, you can prevent Sketch from adding "copy" to their names - Go to Preferences > Layers > Uncheck "Rename Duplicated Layers" 37 | 38 | ## Notes 39 | 40 | 1. You can have different stylesheets in different pages, the layout gets applied on the current page only. 41 | 2. If a layer group has a style, all of it's children are automatically part of the layout. 42 | 3. Adding a layer named **"bg"** stretches it to the size of the group. This is because unlike in HTML, groups have no default background. 43 | 4. Class names are unique across the page and prototypes - if you have a class ".picture" in a prototype and ".picture" in a different one or the stylesheet, only one of them gets applied. 44 | 45 | ## Todos / Known problems 46 | 47 | - See [Issues](https://github.com/hrescak/Sketch-Flex-Layout/issues) to the right. 48 | --------------------------------------------------------------------------------