├── .gitignore ├── .jshintrc ├── README.md ├── bower.json ├── docs ├── item.md ├── notes.md └── outlayer.md ├── item.js ├── outlayer.js ├── package.json ├── sandbox ├── browserify │ ├── browserify.html │ └── main.js ├── cells-by-row.html ├── cells-by-row.js ├── examples.css ├── fit-rows.js ├── item-methods.html ├── padding-percent.html ├── requirejs │ ├── main.js │ └── requirejs.html ├── stagger.html └── toggler.html └── test ├── .jshintrc ├── helpers.js ├── index.html ├── tests.css └── unit ├── add-items.js ├── basics.js ├── container-size.js ├── create.js ├── declarative.js ├── defaults.js ├── destroy.js ├── filter-find.js ├── get-measurements.js ├── hide-reveal.js ├── item-on-transition-end.js ├── jquery-plugin.js ├── layout.js ├── offset.js ├── options.js ├── origin.js ├── percent-position.js ├── prepend.js ├── remove.js ├── stamp.js └── transition-duration.js /.gitignore: -------------------------------------------------------------------------------- 1 | components/ 2 | bower_components/ 3 | node_modules/ 4 | npm-debug.log 5 | sandbox/**/bundle.js 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "strict": true, 4 | "undef": true, 5 | "unused": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Outlayer 2 | 3 | _Brains and guts of a layout library_ 4 | 5 | Outlayer is a base layout class for layout libraries like [Isotope](http://isotope.metafizzy.co), [Packery](http://packery.metafizzy.co), and [Masonry](http://masonry.desandro.com) 6 | 7 | Outlayer layouts work with a container element and children item elements. 8 | 9 | ``` html 10 |
11 |
12 |
13 |
14 | ... 15 |
16 | ``` 17 | 18 | ## Install 19 | 20 | Install with [Bower](http://bower.io): `bower install outlayer` 21 | 22 | [Install with npm](http://npmjs.org/package/outlayer): `npm install outlayer` 23 | 24 | ## Outlayer.create() 25 | 26 | Create a layout class with `Outlayer.create()` 27 | 28 | ``` js 29 | var Layout = Outlayer.create( namespace ); 30 | // for example 31 | var Masonry = Outlayer.create('masonry'); 32 | ``` 33 | 34 | + `namespace` _{String}_ should be camelCased 35 | + returns `LayoutClass` _{Function}_ 36 | 37 | Create a new layout class. `namespace` is used for jQuery plugin, and for declarative initialization. 38 | 39 | The `Layout` inherits from [`Outlayer.prototype`](docs/outlayer.md). 40 | 41 | ``` 42 | var elem = document.querySelector('.selector'); 43 | var msnry = new Masonry( elem, { 44 | // set options... 45 | columnWidth: 200 46 | }); 47 | ``` 48 | 49 | ## Item 50 | 51 | Layouts work with Items, accessible as `Layout.Item`. See [Item API](docs/item.md). 52 | 53 | ## Declarative 54 | 55 | An Outlayer layout class can be initialized via HTML, by setting an attribute of `data-namespace` on the element. Options are set in JSON. For example: 56 | 57 | ``` html 58 | 59 |
60 | ... 61 |
62 | ``` 63 | 64 | The declarative attributes and class will be dashed. i.e. `Outlayer.create('myNiceLayout')` will use `data-my-nice-layout` as the attribute. 65 | 66 | ## .data() 67 | 68 | Get a layout instance from an element. 69 | 70 | ``` 71 | var myMasonry = Masonry.data( document.querySelector('.grid') ); 72 | ``` 73 | 74 | ## jQuery plugin 75 | 76 | The layout class also works as jQuery plugin. 77 | 78 | ``` js 79 | // create Masonry layout class, namespace will be the jQuery method 80 | var Masonry = Outlayer.create('masonry'); 81 | // rock some jQuery 82 | $( function() { 83 | // .masonry() to initialize 84 | var $grid = $('.grid').masonry({ 85 | // options... 86 | }); 87 | // methods are available by passing a string as first parameter 88 | $grid.masonry( 'reveal', elems ); 89 | }); 90 | ``` 91 | 92 | ## RequireJS 93 | 94 | To use Outlayer with [RequireJS](http://requirejs.org/), you'll need to set some config. 95 | 96 | Set [baseUrl](http://requirejs.org/docs/api.html#config-baseUrl) to bower_components and set a [path config](http://requirejs.org/docs/api.html#config-paths) for all your application code. 97 | 98 | ``` js 99 | requirejs.config({ 100 | baseUrl: 'bower_components', 101 | paths: { 102 | app: '../' 103 | } 104 | }); 105 | 106 | requirejs( [ 'outlayer/outlayer', 'app/my-component.js' ], function( Outlayer, myComp ) { 107 | new Outlayer( /*...*/ ) 108 | }); 109 | ``` 110 | 111 | Or set a path config for all Outlayer dependencies. 112 | 113 | ``` js 114 | requirejs.config({ 115 | paths: { 116 | 'ev-emitter': 'bower_components/ev-emitter', 117 | 'get-size': 'bower_components/get-size', 118 | 'desandro-matches-selector': 'bower_components/desandro-matches-selector' 119 | } 120 | }); 121 | ``` 122 | 123 | ## MIT license 124 | 125 | Outlayer is released under the [MIT license](http://desandro.mit-license.org). 126 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outlayer", 3 | "description": "the brains and guts of a layout library", 4 | "main": "outlayer.js", 5 | "dependencies": { 6 | "ev-emitter": "^1.0.0", 7 | "get-size": "^2.0.2", 8 | "fizzy-ui-utils": "^2.0.0" 9 | }, 10 | "devDependencies": { 11 | "jquery-bridget": "2.x", 12 | "jquery": ">=1.4.3 <4", 13 | "qunit": "^2.0.0" 14 | }, 15 | "ignore": [ 16 | "test/", 17 | "docs/", 18 | "sandbox/", 19 | ".*", 20 | "notes.md", 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "tests", 26 | "package.json" 27 | ], 28 | "homepage": "https://github.com/metafizzy/outlayer", 29 | "authors": [ 30 | "Metafizzy" 31 | ], 32 | "moduleType": [ 33 | "amd", 34 | "globals", 35 | "node" 36 | ], 37 | "keywords": [ 38 | "layout", 39 | "masonry", 40 | "isotope" 41 | ], 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /docs/item.md: -------------------------------------------------------------------------------- 1 | # Item 2 | 3 | ## Prototype methods 4 | 5 | ``` js 6 | 7 | Item.prototype._create = function() {} 8 | 9 | Item.prototype.getSize = function() {} 10 | 11 | /** 12 | * apply CSS styles to element 13 | * @param {Object} style 14 | */ 15 | Item.prototype.css = function( style ) {} 16 | 17 | // measure position, and sets it 18 | Item.prototype.getPosition = function() {} 19 | 20 | // moves position with transition 21 | Item.prototype.moveTo = function( x, y ) {} 22 | 23 | // moves position instantly 24 | Item.prototype.goTo = function( x, y ) {} 25 | 26 | /** 27 | * sets CSS transition 28 | * @param {Object} args - arguments 29 | * @param {Object} to - style to transition to 30 | * @param {Object} from - style to start transition from 31 | * @param {Boolean} isCleaning - removes transition styles after transition 32 | * @param {Function} onTransitionEnd - callback 33 | */ 34 | Item.prototype.transition = function( args ) {} 35 | 36 | /** 37 | * removes style property from element 38 | * @param {Object} style 39 | **/ 40 | Item.prototype._removeStyles = function( style ) {} 41 | 42 | Item.prototype.removeTransitionStyles = function() {} 43 | 44 | // hides, then removes element from DOM 45 | Item.prototype.remove = function() {}; 46 | 47 | Item.prototype.reveal = function() {} 48 | 49 | Item.prototype.hide = function() {} 50 | 51 | Item.prototype.destroy = function() {} 52 | ``` 53 | 54 | ## Options 55 | 56 | + transitionDuration 57 | + hiddenStyle 58 | + visibleStyle 59 | -------------------------------------------------------------------------------- /docs/notes.md: -------------------------------------------------------------------------------- 1 | ## need test for 2 | 3 | Check that text entity don't get added as Items 4 | 5 | ignore 6 | unignore 7 | layoutItems does not layout ignored items 8 | 9 | ## To do 10 | 11 | ## docs 12 | 13 | ### methods you can overwrite 14 | 15 | Outlayer._layoutItem( item ) 16 | 17 | Outlayer._resetLayout() 18 | 19 | Outlayer._postLayout() 20 | 21 | Outlayer._getContainerSize() 22 | 23 | Outlayer._create() 24 | 25 | Outlayer._manageStamp( stamp ) 26 | 27 | ### Possibly 28 | 29 | Outlayer.resize() 30 | 31 | Outlayer.destroy() 32 | -------------------------------------------------------------------------------- /docs/outlayer.md: -------------------------------------------------------------------------------- 1 | # Outlayer 2 | 3 | ``` js 4 | /** 5 | * @param {Element, String} element 6 | * @param {Object} options 7 | * @constructor 8 | */ 9 | function Outlayer( element, options ) {} 10 | ``` 11 | 12 | ## Prototype methods 13 | 14 | ``` js 15 | /** 16 | * set options 17 | * @param {Object} opts 18 | */ 19 | Outlayer.prototype.option = function( opts ) {} 20 | 21 | Outlayer.prototype._create = function() {} 22 | 23 | // goes through all children again and gets bricks in proper order 24 | Outlayer.prototype.reloadItems = function() {} 25 | 26 | /** 27 | * get item elements to be used in layout 28 | * @param {Array or NodeList or HTMLElement} elems 29 | * @returns {Array} items - collection of new Outlayer Items 30 | */ 31 | Outlayer.prototype._getItems = function( elems ) {} 32 | 33 | /** 34 | * get item elements to be used in layout 35 | * @param {Array or NodeList or HTMLElement} elems 36 | * @returns {Array} items - item elements 37 | */ 38 | Outlayer.prototype._filterFindItemElements = function( elems ) {} 39 | 40 | /** 41 | * getter method for getting item elements 42 | * @returns {Array} elems - collection of item elements 43 | */ 44 | Outlayer.prototype.getItemElements = function() {} 45 | 46 | // ----- layout ----- // 47 | 48 | /** 49 | * lays out all items 50 | */ 51 | Outlayer.prototype.layout = function() {} 52 | 53 | /** 54 | * logic before any new layout 55 | */ 56 | Outlayer.prototype._resetLayout = function() {} 57 | 58 | Outlayer.prototype.getSize = function() {} 59 | 60 | /** 61 | * get measurement from option, for columnWidth, rowHeight, gutter 62 | * if option is String -> get element from selector string, & get size of element 63 | * if option is Element -> get size of element 64 | * else use option as a number 65 | * 66 | * @param {String} measurement 67 | * @param {String} size - width or height 68 | * @private 69 | */ 70 | Outlayer.prototype._getMeasurement = function( measurement, size ) {} 71 | 72 | /** 73 | * layout a collection of item elements 74 | */ 75 | Outlayer.prototype.layoutItems = function( items, isInstant ) {} 76 | 77 | /** 78 | * Sets position of item in DOM 79 | * @param {Outlayer.Item} item 80 | * @param {Number} x - horizontal position 81 | * @param {Number} y - vertical position 82 | * @param {Boolean} isInstant - disables transitions 83 | */ 84 | Outlayer.prototype._layoutItem = function( item, x, y, isInstant ) {} 85 | 86 | /** 87 | * trigger a callback for a collection of items events 88 | * @param {Array} items - Outlayer.Items 89 | * @param {String} eventName 90 | * @param {Function} callback 91 | */ 92 | Outlayer.prototype._itemsOn = function( items, eventName, callback ) {} 93 | 94 | // ----- resize ----- // 95 | 96 | /** 97 | * Bind layout to window resizing 98 | */ 99 | Outlayer.prototype.bindResize = function() {} 100 | 101 | /** 102 | * Unbind layout to window resizing 103 | */ 104 | Outlayer.prototype.unbindResize = function() { 105 | 106 | // debounced, layout on resize 107 | Outlayer.prototype.resize = function() {} 108 | 109 | // -------------------------- methods -------------------------- // 110 | 111 | /** 112 | * add items to Outlayer instance 113 | * @param {Array or NodeList or Element} elems 114 | * @returns {Array} items - Outlayer.Items 115 | **/ 116 | Outlayer.prototype.addItems = function( elems ) {} 117 | 118 | /** 119 | * Layout newly-appended item elements 120 | * @param {Array or NodeList or Element} elems 121 | */ 122 | Outlayer.prototype.appended = function( elems ) {} 123 | 124 | /** 125 | * Layout prepended elements 126 | * @param {Array or NodeList or Element} elems 127 | */ 128 | Outlayer.prototype.prepended = function( elems ) {} 129 | 130 | // reveal a collection of items 131 | Outlayer.prototype.reveal = function( items ) {} 132 | 133 | /** 134 | * get Outlayer.Item, given an Element 135 | * @param {Element} elem 136 | * @param {Function} callback 137 | * @returns {Outlayer.Item} item 138 | */ 139 | Outlayer.prototype.getItem = function( elem ) {} 140 | 141 | /** 142 | * get collection of Outlayer.Items, given Elements 143 | * @param {Array} elems 144 | * @returns {Array} items - Outlayer.Items 145 | */ 146 | Outlayer.prototype.getItems = function( elems ) {} 147 | 148 | /** 149 | * remove element(s) from instance and DOM 150 | * @param {Array or NodeList or Element} elems 151 | */ 152 | Outlayer.prototype.remove = function( elems ) {} 153 | 154 | /** 155 | * reveal a collection of items 156 | * @param {Array of Outlayer.Items} items 157 | */ 158 | Outlayer.prototype.reveal = function( items ) {} 159 | 160 | /** 161 | * hide a collection of items 162 | * @param {Array of Outlayer.Items} items 163 | */ 164 | Outlayer.prototype.hide = function( items ) {} 165 | 166 | /** 167 | * reveal item elements 168 | * @param {Array}, {Element}, {NodeList} items 169 | */ 170 | Outlayer.prototype.revealItemElements = function( elems ) {} 171 | 172 | /** 173 | * hide item elements 174 | * @param {Array}, {Element}, {NodeList} items 175 | */ 176 | Outlayer.prototype.hideItemElements = function( elems ) {} 177 | 178 | // remove and disable Outlayer instance 179 | Outlayer.prototype.destroy = function() {} 180 | 181 | ``` 182 | 183 | ## Utility methods 184 | 185 | ``` js 186 | 187 | /** 188 | * get Outlayer instance from element 189 | * @param {Element} elem 190 | * @returns {Outlayer} 191 | */ 192 | Outlayer.data = function( elem ) {} 193 | 194 | /** 195 | * create a layout class 196 | * @param {String} namespace 197 | */ 198 | Outlayer.create = function( namespace ) {} 199 | ``` 200 | 201 | 202 | ## Options 203 | 204 | + containerStyle `{ position: 'relative' }` 205 | + initLayout or isInitLayout `true` 206 | + layoutInstant or isLayoutInstant 207 | + originLeft or isOriginLeft `true` 208 | + originTop or isOriginTop `true` 209 | + resize or isResizeBound `true` 210 | + resizeContainer or isResizingContainer `true` 211 | + itemOptions 212 | + itemSelector 213 | + stamp 214 | + percentPosition 215 | -------------------------------------------------------------------------------- /item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Outlayer Item 3 | */ 4 | 5 | ( function( window, factory ) { 6 | // universal module definition 7 | /* jshint strict: false */ /* globals define, module, require */ 8 | if ( typeof define == 'function' && define.amd ) { 9 | // AMD - RequireJS 10 | define( [ 11 | 'ev-emitter/ev-emitter', 12 | 'get-size/get-size' 13 | ], 14 | factory 15 | ); 16 | } else if ( typeof module == 'object' && module.exports ) { 17 | // CommonJS - Browserify, Webpack 18 | module.exports = factory( 19 | require('ev-emitter'), 20 | require('get-size') 21 | ); 22 | } else { 23 | // browser global 24 | window.Outlayer = {}; 25 | window.Outlayer.Item = factory( 26 | window.EvEmitter, 27 | window.getSize 28 | ); 29 | } 30 | 31 | }( window, function factory( EvEmitter, getSize ) { 32 | 'use strict'; 33 | 34 | // ----- helpers ----- // 35 | 36 | function isEmptyObj( obj ) { 37 | for ( var prop in obj ) { 38 | return false; 39 | } 40 | prop = null; 41 | return true; 42 | } 43 | 44 | // -------------------------- CSS3 support -------------------------- // 45 | 46 | 47 | var docElemStyle = document.documentElement.style; 48 | 49 | var transitionProperty = typeof docElemStyle.transition == 'string' ? 50 | 'transition' : 'WebkitTransition'; 51 | var transformProperty = typeof docElemStyle.transform == 'string' ? 52 | 'transform' : 'WebkitTransform'; 53 | 54 | var transitionEndEvent = { 55 | WebkitTransition: 'webkitTransitionEnd', 56 | transition: 'transitionend' 57 | }[ transitionProperty ]; 58 | 59 | // cache all vendor properties that could have vendor prefix 60 | var vendorProperties = { 61 | transform: transformProperty, 62 | transition: transitionProperty, 63 | transitionDuration: transitionProperty + 'Duration', 64 | transitionProperty: transitionProperty + 'Property', 65 | transitionDelay: transitionProperty + 'Delay' 66 | }; 67 | 68 | // -------------------------- Item -------------------------- // 69 | 70 | function Item( element, layout ) { 71 | if ( !element ) { 72 | return; 73 | } 74 | 75 | this.element = element; 76 | // parent layout class, i.e. Masonry, Isotope, or Packery 77 | this.layout = layout; 78 | this.position = { 79 | x: 0, 80 | y: 0 81 | }; 82 | 83 | this._create(); 84 | } 85 | 86 | // inherit EvEmitter 87 | var proto = Item.prototype = Object.create( EvEmitter.prototype ); 88 | proto.constructor = Item; 89 | 90 | proto._create = function() { 91 | // transition objects 92 | this._transn = { 93 | ingProperties: {}, 94 | clean: {}, 95 | onEnd: {} 96 | }; 97 | 98 | this.css({ 99 | position: 'absolute' 100 | }); 101 | }; 102 | 103 | // trigger specified handler for event type 104 | proto.handleEvent = function( event ) { 105 | var method = 'on' + event.type; 106 | if ( this[ method ] ) { 107 | this[ method ]( event ); 108 | } 109 | }; 110 | 111 | proto.getSize = function() { 112 | this.size = getSize( this.element ); 113 | }; 114 | 115 | /** 116 | * apply CSS styles to element 117 | * @param {Object} style 118 | */ 119 | proto.css = function( style ) { 120 | var elemStyle = this.element.style; 121 | 122 | for ( var prop in style ) { 123 | // use vendor property if available 124 | var supportedProp = vendorProperties[ prop ] || prop; 125 | elemStyle[ supportedProp ] = style[ prop ]; 126 | } 127 | }; 128 | 129 | // measure position, and sets it 130 | proto.getPosition = function() { 131 | var style = getComputedStyle( this.element ); 132 | var isOriginLeft = this.layout._getOption('originLeft'); 133 | var isOriginTop = this.layout._getOption('originTop'); 134 | var xValue = style[ isOriginLeft ? 'left' : 'right' ]; 135 | var yValue = style[ isOriginTop ? 'top' : 'bottom' ]; 136 | var x = parseFloat( xValue ); 137 | var y = parseFloat( yValue ); 138 | // convert percent to pixels 139 | var layoutSize = this.layout.size; 140 | if ( xValue.indexOf('%') != -1 ) { 141 | x = ( x / 100 ) * layoutSize.width; 142 | } 143 | if ( yValue.indexOf('%') != -1 ) { 144 | y = ( y / 100 ) * layoutSize.height; 145 | } 146 | // clean up 'auto' or other non-integer values 147 | x = isNaN( x ) ? 0 : x; 148 | y = isNaN( y ) ? 0 : y; 149 | // remove padding from measurement 150 | x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight; 151 | y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom; 152 | 153 | this.position.x = x; 154 | this.position.y = y; 155 | }; 156 | 157 | // set settled position, apply padding 158 | proto.layoutPosition = function() { 159 | var layoutSize = this.layout.size; 160 | var style = {}; 161 | var isOriginLeft = this.layout._getOption('originLeft'); 162 | var isOriginTop = this.layout._getOption('originTop'); 163 | 164 | // x 165 | var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight'; 166 | var xProperty = isOriginLeft ? 'left' : 'right'; 167 | var xResetProperty = isOriginLeft ? 'right' : 'left'; 168 | 169 | var x = this.position.x + layoutSize[ xPadding ]; 170 | // set in percentage or pixels 171 | style[ xProperty ] = this.getXValue( x ); 172 | // reset other property 173 | style[ xResetProperty ] = ''; 174 | 175 | // y 176 | var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom'; 177 | var yProperty = isOriginTop ? 'top' : 'bottom'; 178 | var yResetProperty = isOriginTop ? 'bottom' : 'top'; 179 | 180 | var y = this.position.y + layoutSize[ yPadding ]; 181 | // set in percentage or pixels 182 | style[ yProperty ] = this.getYValue( y ); 183 | // reset other property 184 | style[ yResetProperty ] = ''; 185 | 186 | this.css( style ); 187 | this.emitEvent( 'layout', [ this ] ); 188 | }; 189 | 190 | proto.getXValue = function( x ) { 191 | var isHorizontal = this.layout._getOption('horizontal'); 192 | return this.layout.options.percentPosition && !isHorizontal ? 193 | ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px'; 194 | }; 195 | 196 | proto.getYValue = function( y ) { 197 | var isHorizontal = this.layout._getOption('horizontal'); 198 | return this.layout.options.percentPosition && isHorizontal ? 199 | ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px'; 200 | }; 201 | 202 | proto._transitionTo = function( x, y ) { 203 | this.getPosition(); 204 | // get current x & y from top/left 205 | var curX = this.position.x; 206 | var curY = this.position.y; 207 | 208 | var didNotMove = x == this.position.x && y == this.position.y; 209 | 210 | // save end position 211 | this.setPosition( x, y ); 212 | 213 | // if did not move and not transitioning, just go to layout 214 | if ( didNotMove && !this.isTransitioning ) { 215 | this.layoutPosition(); 216 | return; 217 | } 218 | 219 | var transX = x - curX; 220 | var transY = y - curY; 221 | var transitionStyle = {}; 222 | transitionStyle.transform = this.getTranslate( transX, transY ); 223 | 224 | this.transition({ 225 | to: transitionStyle, 226 | onTransitionEnd: { 227 | transform: this.layoutPosition 228 | }, 229 | isCleaning: true 230 | }); 231 | }; 232 | 233 | proto.getTranslate = function( x, y ) { 234 | // flip cooridinates if origin on right or bottom 235 | var isOriginLeft = this.layout._getOption('originLeft'); 236 | var isOriginTop = this.layout._getOption('originTop'); 237 | x = isOriginLeft ? x : -x; 238 | y = isOriginTop ? y : -y; 239 | return 'translate3d(' + x + 'px, ' + y + 'px, 0)'; 240 | }; 241 | 242 | // non transition + transform support 243 | proto.goTo = function( x, y ) { 244 | this.setPosition( x, y ); 245 | this.layoutPosition(); 246 | }; 247 | 248 | proto.moveTo = proto._transitionTo; 249 | 250 | proto.setPosition = function( x, y ) { 251 | this.position.x = parseFloat( x ); 252 | this.position.y = parseFloat( y ); 253 | }; 254 | 255 | // ----- transition ----- // 256 | 257 | /** 258 | * @param {Object} style - CSS 259 | * @param {Function} onTransitionEnd 260 | */ 261 | 262 | // non transition, just trigger callback 263 | proto._nonTransition = function( args ) { 264 | this.css( args.to ); 265 | if ( args.isCleaning ) { 266 | this._removeStyles( args.to ); 267 | } 268 | for ( var prop in args.onTransitionEnd ) { 269 | args.onTransitionEnd[ prop ].call( this ); 270 | } 271 | }; 272 | 273 | /** 274 | * proper transition 275 | * @param {Object} args - arguments 276 | * @param {Object} to - style to transition to 277 | * @param {Object} from - style to start transition from 278 | * @param {Boolean} isCleaning - removes transition styles after transition 279 | * @param {Function} onTransitionEnd - callback 280 | */ 281 | proto.transition = function( args ) { 282 | // redirect to nonTransition if no transition duration 283 | if ( !parseFloat( this.layout.options.transitionDuration ) ) { 284 | this._nonTransition( args ); 285 | return; 286 | } 287 | 288 | var _transition = this._transn; 289 | // keep track of onTransitionEnd callback by css property 290 | for ( var prop in args.onTransitionEnd ) { 291 | _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ]; 292 | } 293 | // keep track of properties that are transitioning 294 | for ( prop in args.to ) { 295 | _transition.ingProperties[ prop ] = true; 296 | // keep track of properties to clean up when transition is done 297 | if ( args.isCleaning ) { 298 | _transition.clean[ prop ] = true; 299 | } 300 | } 301 | 302 | // set from styles 303 | if ( args.from ) { 304 | this.css( args.from ); 305 | // force redraw. http://blog.alexmaccaw.com/css-transitions 306 | var h = this.element.offsetHeight; 307 | // hack for JSHint to hush about unused var 308 | h = null; 309 | } 310 | // enable transition 311 | this.enableTransition( args.to ); 312 | // set styles that are transitioning 313 | this.css( args.to ); 314 | 315 | this.isTransitioning = true; 316 | 317 | }; 318 | 319 | // dash before all cap letters, including first for 320 | // WebkitTransform => -webkit-transform 321 | function toDashedAll( str ) { 322 | return str.replace( /([A-Z])/g, function( $1 ) { 323 | return '-' + $1.toLowerCase(); 324 | }); 325 | } 326 | 327 | var transitionProps = 'opacity,' + toDashedAll( transformProperty ); 328 | 329 | proto.enableTransition = function(/* style */) { 330 | // HACK changing transitionProperty during a transition 331 | // will cause transition to jump 332 | if ( this.isTransitioning ) { 333 | return; 334 | } 335 | 336 | // make `transition: foo, bar, baz` from style object 337 | // HACK un-comment this when enableTransition can work 338 | // while a transition is happening 339 | // var transitionValues = []; 340 | // for ( var prop in style ) { 341 | // // dash-ify camelCased properties like WebkitTransition 342 | // prop = vendorProperties[ prop ] || prop; 343 | // transitionValues.push( toDashedAll( prop ) ); 344 | // } 345 | // munge number to millisecond, to match stagger 346 | var duration = this.layout.options.transitionDuration; 347 | duration = typeof duration == 'number' ? duration + 'ms' : duration; 348 | // enable transition styles 349 | this.css({ 350 | transitionProperty: transitionProps, 351 | transitionDuration: duration, 352 | transitionDelay: this.staggerDelay || 0 353 | }); 354 | // listen for transition end event 355 | this.element.addEventListener( transitionEndEvent, this, false ); 356 | }; 357 | 358 | // ----- events ----- // 359 | 360 | proto.onwebkitTransitionEnd = function( event ) { 361 | this.ontransitionend( event ); 362 | }; 363 | 364 | proto.onotransitionend = function( event ) { 365 | this.ontransitionend( event ); 366 | }; 367 | 368 | // properties that I munge to make my life easier 369 | var dashedVendorProperties = { 370 | '-webkit-transform': 'transform' 371 | }; 372 | 373 | proto.ontransitionend = function( event ) { 374 | // disregard bubbled events from children 375 | if ( event.target !== this.element ) { 376 | return; 377 | } 378 | var _transition = this._transn; 379 | // get property name of transitioned property, convert to prefix-free 380 | var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName; 381 | 382 | // remove property that has completed transitioning 383 | delete _transition.ingProperties[ propertyName ]; 384 | // check if any properties are still transitioning 385 | if ( isEmptyObj( _transition.ingProperties ) ) { 386 | // all properties have completed transitioning 387 | this.disableTransition(); 388 | } 389 | // clean style 390 | if ( propertyName in _transition.clean ) { 391 | // clean up style 392 | this.element.style[ event.propertyName ] = ''; 393 | delete _transition.clean[ propertyName ]; 394 | } 395 | // trigger onTransitionEnd callback 396 | if ( propertyName in _transition.onEnd ) { 397 | var onTransitionEnd = _transition.onEnd[ propertyName ]; 398 | onTransitionEnd.call( this ); 399 | delete _transition.onEnd[ propertyName ]; 400 | } 401 | 402 | this.emitEvent( 'transitionEnd', [ this ] ); 403 | }; 404 | 405 | proto.disableTransition = function() { 406 | this.removeTransitionStyles(); 407 | this.element.removeEventListener( transitionEndEvent, this, false ); 408 | this.isTransitioning = false; 409 | }; 410 | 411 | /** 412 | * removes style property from element 413 | * @param {Object} style 414 | **/ 415 | proto._removeStyles = function( style ) { 416 | // clean up transition styles 417 | var cleanStyle = {}; 418 | for ( var prop in style ) { 419 | cleanStyle[ prop ] = ''; 420 | } 421 | this.css( cleanStyle ); 422 | }; 423 | 424 | var cleanTransitionStyle = { 425 | transitionProperty: '', 426 | transitionDuration: '', 427 | transitionDelay: '' 428 | }; 429 | 430 | proto.removeTransitionStyles = function() { 431 | // remove transition 432 | this.css( cleanTransitionStyle ); 433 | }; 434 | 435 | // ----- stagger ----- // 436 | 437 | proto.stagger = function( delay ) { 438 | delay = isNaN( delay ) ? 0 : delay; 439 | this.staggerDelay = delay + 'ms'; 440 | }; 441 | 442 | // ----- show/hide/remove ----- // 443 | 444 | // remove element from DOM 445 | proto.removeElem = function() { 446 | var parent = this.element.parentNode; 447 | if ( parent ) { 448 | parent.removeChild( this.element ); 449 | } 450 | // remove display: none 451 | this.css({ display: '' }); 452 | this.emitEvent( 'remove', [ this ] ); 453 | }; 454 | 455 | proto.remove = function() { 456 | // just remove element if no transition support or no transition 457 | if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) { 458 | this.removeElem(); 459 | return; 460 | } 461 | 462 | // start transition 463 | this.once( 'transitionEnd', function() { 464 | this.removeElem(); 465 | }); 466 | this.hide(); 467 | }; 468 | 469 | proto.reveal = function() { 470 | delete this.isHidden; 471 | // remove display: none 472 | this.css({ display: '' }); 473 | 474 | var options = this.layout.options; 475 | 476 | var onTransitionEnd = {}; 477 | var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle'); 478 | onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd; 479 | 480 | this.transition({ 481 | from: options.hiddenStyle, 482 | to: options.visibleStyle, 483 | isCleaning: true, 484 | onTransitionEnd: onTransitionEnd 485 | }); 486 | }; 487 | 488 | proto.onRevealTransitionEnd = function() { 489 | // check if still visible 490 | // during transition, item may have been hidden 491 | if ( !this.isHidden ) { 492 | this.emitEvent('reveal'); 493 | } 494 | }; 495 | 496 | /** 497 | * get style property use for hide/reveal transition end 498 | * @param {String} styleProperty - hiddenStyle/visibleStyle 499 | * @returns {String} 500 | */ 501 | proto.getHideRevealTransitionEndProperty = function( styleProperty ) { 502 | var optionStyle = this.layout.options[ styleProperty ]; 503 | // use opacity 504 | if ( optionStyle.opacity ) { 505 | return 'opacity'; 506 | } 507 | // get first property 508 | for ( var prop in optionStyle ) { 509 | return prop; 510 | } 511 | }; 512 | 513 | proto.hide = function() { 514 | // set flag 515 | this.isHidden = true; 516 | // remove display: none 517 | this.css({ display: '' }); 518 | 519 | var options = this.layout.options; 520 | 521 | var onTransitionEnd = {}; 522 | var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle'); 523 | onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd; 524 | 525 | this.transition({ 526 | from: options.visibleStyle, 527 | to: options.hiddenStyle, 528 | // keep hidden stuff hidden 529 | isCleaning: true, 530 | onTransitionEnd: onTransitionEnd 531 | }); 532 | }; 533 | 534 | proto.onHideTransitionEnd = function() { 535 | // check if still hidden 536 | // during transition, item may have been un-hidden 537 | if ( this.isHidden ) { 538 | this.css({ display: 'none' }); 539 | this.emitEvent('hide'); 540 | } 541 | }; 542 | 543 | proto.destroy = function() { 544 | this.css({ 545 | position: '', 546 | left: '', 547 | right: '', 548 | top: '', 549 | bottom: '', 550 | transition: '', 551 | transform: '' 552 | }); 553 | }; 554 | 555 | return Item; 556 | 557 | })); 558 | -------------------------------------------------------------------------------- /outlayer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Outlayer v2.1.1 3 | * the brains and guts of a layout library 4 | * MIT license 5 | */ 6 | 7 | ( function( window, factory ) { 8 | 'use strict'; 9 | // universal module definition 10 | /* jshint strict: false */ /* globals define, module, require */ 11 | if ( typeof define == 'function' && define.amd ) { 12 | // AMD - RequireJS 13 | define( [ 14 | 'ev-emitter/ev-emitter', 15 | 'get-size/get-size', 16 | 'fizzy-ui-utils/utils', 17 | './item' 18 | ], 19 | function( EvEmitter, getSize, utils, Item ) { 20 | return factory( window, EvEmitter, getSize, utils, Item); 21 | } 22 | ); 23 | } else if ( typeof module == 'object' && module.exports ) { 24 | // CommonJS - Browserify, Webpack 25 | module.exports = factory( 26 | window, 27 | require('ev-emitter'), 28 | require('get-size'), 29 | require('fizzy-ui-utils'), 30 | require('./item') 31 | ); 32 | } else { 33 | // browser global 34 | window.Outlayer = factory( 35 | window, 36 | window.EvEmitter, 37 | window.getSize, 38 | window.fizzyUIUtils, 39 | window.Outlayer.Item 40 | ); 41 | } 42 | 43 | }( window, function factory( window, EvEmitter, getSize, utils, Item ) { 44 | 'use strict'; 45 | 46 | // ----- vars ----- // 47 | 48 | var console = window.console; 49 | var jQuery = window.jQuery; 50 | var noop = function() {}; 51 | 52 | // -------------------------- Outlayer -------------------------- // 53 | 54 | // globally unique identifiers 55 | var GUID = 0; 56 | // internal store of all Outlayer intances 57 | var instances = {}; 58 | 59 | 60 | /** 61 | * @param {Element, String} element 62 | * @param {Object} options 63 | * @constructor 64 | */ 65 | function Outlayer( element, options ) { 66 | var queryElement = utils.getQueryElement( element ); 67 | if ( !queryElement ) { 68 | if ( console ) { 69 | console.error( 'Bad element for ' + this.constructor.namespace + 70 | ': ' + ( queryElement || element ) ); 71 | } 72 | return; 73 | } 74 | this.element = queryElement; 75 | // add jQuery 76 | if ( jQuery ) { 77 | this.$element = jQuery( this.element ); 78 | } 79 | 80 | // options 81 | this.options = utils.extend( {}, this.constructor.defaults ); 82 | this.option( options ); 83 | 84 | // add id for Outlayer.getFromElement 85 | var id = ++GUID; 86 | this.element.outlayerGUID = id; // expando 87 | instances[ id ] = this; // associate via id 88 | 89 | // kick it off 90 | this._create(); 91 | 92 | var isInitLayout = this._getOption('initLayout'); 93 | if ( isInitLayout ) { 94 | this.layout(); 95 | } 96 | } 97 | 98 | // settings are for internal use only 99 | Outlayer.namespace = 'outlayer'; 100 | Outlayer.Item = Item; 101 | 102 | // default options 103 | Outlayer.defaults = { 104 | containerStyle: { 105 | position: 'relative' 106 | }, 107 | initLayout: true, 108 | originLeft: true, 109 | originTop: true, 110 | resize: true, 111 | resizeContainer: true, 112 | // item options 113 | transitionDuration: '0.4s', 114 | hiddenStyle: { 115 | opacity: 0, 116 | transform: 'scale(0.001)' 117 | }, 118 | visibleStyle: { 119 | opacity: 1, 120 | transform: 'scale(1)' 121 | } 122 | }; 123 | 124 | var proto = Outlayer.prototype; 125 | // inherit EvEmitter 126 | utils.extend( proto, EvEmitter.prototype ); 127 | 128 | /** 129 | * set options 130 | * @param {Object} opts 131 | */ 132 | proto.option = function( opts ) { 133 | utils.extend( this.options, opts ); 134 | }; 135 | 136 | /** 137 | * get backwards compatible option value, check old name 138 | */ 139 | proto._getOption = function( option ) { 140 | var oldOption = this.constructor.compatOptions[ option ]; 141 | return oldOption && this.options[ oldOption ] !== undefined ? 142 | this.options[ oldOption ] : this.options[ option ]; 143 | }; 144 | 145 | Outlayer.compatOptions = { 146 | // currentName: oldName 147 | initLayout: 'isInitLayout', 148 | horizontal: 'isHorizontal', 149 | layoutInstant: 'isLayoutInstant', 150 | originLeft: 'isOriginLeft', 151 | originTop: 'isOriginTop', 152 | resize: 'isResizeBound', 153 | resizeContainer: 'isResizingContainer' 154 | }; 155 | 156 | proto._create = function() { 157 | // get items from children 158 | this.reloadItems(); 159 | // elements that affect layout, but are not laid out 160 | this.stamps = []; 161 | this.stamp( this.options.stamp ); 162 | // set container style 163 | utils.extend( this.element.style, this.options.containerStyle ); 164 | 165 | // bind resize method 166 | var canBindResize = this._getOption('resize'); 167 | if ( canBindResize ) { 168 | this.bindResize(); 169 | } 170 | }; 171 | 172 | // goes through all children again and gets bricks in proper order 173 | proto.reloadItems = function() { 174 | // collection of item elements 175 | this.items = this._itemize( this.element.children ); 176 | }; 177 | 178 | 179 | /** 180 | * turn elements into Outlayer.Items to be used in layout 181 | * @param {Array or NodeList or HTMLElement} elems 182 | * @returns {Array} items - collection of new Outlayer Items 183 | */ 184 | proto._itemize = function( elems ) { 185 | 186 | var itemElems = this._filterFindItemElements( elems ); 187 | var Item = this.constructor.Item; 188 | 189 | // create new Outlayer Items for collection 190 | var items = []; 191 | for ( var i=0; i < itemElems.length; i++ ) { 192 | var elem = itemElems[i]; 193 | var item = new Item( elem, this ); 194 | items.push( item ); 195 | } 196 | 197 | return items; 198 | }; 199 | 200 | /** 201 | * get item elements to be used in layout 202 | * @param {Array or NodeList or HTMLElement} elems 203 | * @returns {Array} items - item elements 204 | */ 205 | proto._filterFindItemElements = function( elems ) { 206 | return utils.filterFindElements( elems, this.options.itemSelector ); 207 | }; 208 | 209 | /** 210 | * getter method for getting item elements 211 | * @returns {Array} elems - collection of item elements 212 | */ 213 | proto.getItemElements = function() { 214 | return this.items.map( function( item ) { 215 | return item.element; 216 | }); 217 | }; 218 | 219 | // ----- init & layout ----- // 220 | 221 | /** 222 | * lays out all items 223 | */ 224 | proto.layout = function() { 225 | this._resetLayout(); 226 | this._manageStamps(); 227 | 228 | // don't animate first layout 229 | var layoutInstant = this._getOption('layoutInstant'); 230 | var isInstant = layoutInstant !== undefined ? 231 | layoutInstant : !this._isLayoutInited; 232 | this.layoutItems( this.items, isInstant ); 233 | 234 | // flag for initalized 235 | this._isLayoutInited = true; 236 | }; 237 | 238 | // _init is alias for layout 239 | proto._init = proto.layout; 240 | 241 | /** 242 | * logic before any new layout 243 | */ 244 | proto._resetLayout = function() { 245 | this.getSize(); 246 | }; 247 | 248 | 249 | proto.getSize = function() { 250 | this.size = getSize( this.element ); 251 | }; 252 | 253 | /** 254 | * get measurement from option, for columnWidth, rowHeight, gutter 255 | * if option is String -> get element from selector string, & get size of element 256 | * if option is Element -> get size of element 257 | * else use option as a number 258 | * 259 | * @param {String} measurement 260 | * @param {String} size - width or height 261 | * @private 262 | */ 263 | proto._getMeasurement = function( measurement, size ) { 264 | var option = this.options[ measurement ]; 265 | var elem; 266 | if ( !option ) { 267 | // default to 0 268 | this[ measurement ] = 0; 269 | } else { 270 | // use option as an element 271 | if ( typeof option == 'string' ) { 272 | elem = this.element.querySelector( option ); 273 | } else if ( option instanceof HTMLElement ) { 274 | elem = option; 275 | } 276 | // use size of element, if element 277 | this[ measurement ] = elem ? getSize( elem )[ size ] : option; 278 | } 279 | }; 280 | 281 | /** 282 | * layout a collection of item elements 283 | * @api public 284 | */ 285 | proto.layoutItems = function( items, isInstant ) { 286 | items = this._getItemsForLayout( items ); 287 | 288 | this._layoutItems( items, isInstant ); 289 | 290 | this._postLayout(); 291 | }; 292 | 293 | /** 294 | * get the items to be laid out 295 | * you may want to skip over some items 296 | * @param {Array} items 297 | * @returns {Array} items 298 | */ 299 | proto._getItemsForLayout = function( items ) { 300 | return items.filter( function( item ) { 301 | return !item.isIgnored; 302 | }); 303 | }; 304 | 305 | /** 306 | * layout items 307 | * @param {Array} items 308 | * @param {Boolean} isInstant 309 | */ 310 | proto._layoutItems = function( items, isInstant ) { 311 | this._emitCompleteOnItems( 'layout', items ); 312 | 313 | if ( !items || !items.length ) { 314 | // no items, emit event with empty array 315 | return; 316 | } 317 | 318 | var queue = []; 319 | 320 | items.forEach( function( item ) { 321 | // get x/y object from method 322 | var position = this._getItemLayoutPosition( item ); 323 | // enqueue 324 | position.item = item; 325 | position.isInstant = isInstant || item.isLayoutInstant; 326 | queue.push( position ); 327 | }, this ); 328 | 329 | this._processLayoutQueue( queue ); 330 | }; 331 | 332 | /** 333 | * get item layout position 334 | * @param {Outlayer.Item} item 335 | * @returns {Object} x and y position 336 | */ 337 | proto._getItemLayoutPosition = function( /* item */ ) { 338 | return { 339 | x: 0, 340 | y: 0 341 | }; 342 | }; 343 | 344 | /** 345 | * iterate over array and position each item 346 | * Reason being - separating this logic prevents 'layout invalidation' 347 | * thx @paul_irish 348 | * @param {Array} queue 349 | */ 350 | proto._processLayoutQueue = function( queue ) { 351 | this.updateStagger(); 352 | queue.forEach( function( obj, i ) { 353 | this._positionItem( obj.item, obj.x, obj.y, obj.isInstant, i ); 354 | }, this ); 355 | }; 356 | 357 | // set stagger from option in milliseconds number 358 | proto.updateStagger = function() { 359 | var stagger = this.options.stagger; 360 | if ( stagger === null || stagger === undefined ) { 361 | this.stagger = 0; 362 | return; 363 | } 364 | this.stagger = getMilliseconds( stagger ); 365 | return this.stagger; 366 | }; 367 | 368 | /** 369 | * Sets position of item in DOM 370 | * @param {Outlayer.Item} item 371 | * @param {Number} x - horizontal position 372 | * @param {Number} y - vertical position 373 | * @param {Boolean} isInstant - disables transitions 374 | */ 375 | proto._positionItem = function( item, x, y, isInstant, i ) { 376 | if ( isInstant ) { 377 | // if not transition, just set CSS 378 | item.goTo( x, y ); 379 | } else { 380 | item.stagger( i * this.stagger ); 381 | item.moveTo( x, y ); 382 | } 383 | }; 384 | 385 | /** 386 | * Any logic you want to do after each layout, 387 | * i.e. size the container 388 | */ 389 | proto._postLayout = function() { 390 | this.resizeContainer(); 391 | }; 392 | 393 | proto.resizeContainer = function() { 394 | var isResizingContainer = this._getOption('resizeContainer'); 395 | if ( !isResizingContainer ) { 396 | return; 397 | } 398 | var size = this._getContainerSize(); 399 | if ( size ) { 400 | this._setContainerMeasure( size.width, true ); 401 | this._setContainerMeasure( size.height, false ); 402 | } 403 | }; 404 | 405 | /** 406 | * Sets width or height of container if returned 407 | * @returns {Object} size 408 | * @param {Number} width 409 | * @param {Number} height 410 | */ 411 | proto._getContainerSize = noop; 412 | 413 | /** 414 | * @param {Number} measure - size of width or height 415 | * @param {Boolean} isWidth 416 | */ 417 | proto._setContainerMeasure = function( measure, isWidth ) { 418 | if ( measure === undefined ) { 419 | return; 420 | } 421 | 422 | var elemSize = this.size; 423 | // add padding and border width if border box 424 | if ( elemSize.isBorderBox ) { 425 | measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight + 426 | elemSize.borderLeftWidth + elemSize.borderRightWidth : 427 | elemSize.paddingBottom + elemSize.paddingTop + 428 | elemSize.borderTopWidth + elemSize.borderBottomWidth; 429 | } 430 | 431 | measure = Math.max( measure, 0 ); 432 | this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px'; 433 | }; 434 | 435 | /** 436 | * emit eventComplete on a collection of items events 437 | * @param {String} eventName 438 | * @param {Array} items - Outlayer.Items 439 | */ 440 | proto._emitCompleteOnItems = function( eventName, items ) { 441 | var _this = this; 442 | function onComplete() { 443 | _this.dispatchEvent( eventName + 'Complete', null, [ items ] ); 444 | } 445 | 446 | var count = items.length; 447 | if ( !items || !count ) { 448 | onComplete(); 449 | return; 450 | } 451 | 452 | var doneCount = 0; 453 | function tick() { 454 | doneCount++; 455 | if ( doneCount == count ) { 456 | onComplete(); 457 | } 458 | } 459 | 460 | // bind callback 461 | items.forEach( function( item ) { 462 | item.once( eventName, tick ); 463 | }); 464 | }; 465 | 466 | /** 467 | * emits events via EvEmitter and jQuery events 468 | * @param {String} type - name of event 469 | * @param {Event} event - original event 470 | * @param {Array} args - extra arguments 471 | */ 472 | proto.dispatchEvent = function( type, event, args ) { 473 | // add original event to arguments 474 | var emitArgs = event ? [ event ].concat( args ) : args; 475 | this.emitEvent( type, emitArgs ); 476 | 477 | if ( jQuery ) { 478 | // set this.$element 479 | this.$element = this.$element || jQuery( this.element ); 480 | if ( event ) { 481 | // create jQuery event 482 | var $event = jQuery.Event( event ); 483 | $event.type = type; 484 | this.$element.trigger( $event, args ); 485 | } else { 486 | // just trigger with type if no event available 487 | this.$element.trigger( type, args ); 488 | } 489 | } 490 | }; 491 | 492 | // -------------------------- ignore & stamps -------------------------- // 493 | 494 | 495 | /** 496 | * keep item in collection, but do not lay it out 497 | * ignored items do not get skipped in layout 498 | * @param {Element} elem 499 | */ 500 | proto.ignore = function( elem ) { 501 | var item = this.getItem( elem ); 502 | if ( item ) { 503 | item.isIgnored = true; 504 | } 505 | }; 506 | 507 | /** 508 | * return item to layout collection 509 | * @param {Element} elem 510 | */ 511 | proto.unignore = function( elem ) { 512 | var item = this.getItem( elem ); 513 | if ( item ) { 514 | delete item.isIgnored; 515 | } 516 | }; 517 | 518 | /** 519 | * adds elements to stamps 520 | * @param {NodeList, Array, Element, or String} elems 521 | */ 522 | proto.stamp = function( elems ) { 523 | elems = this._find( elems ); 524 | if ( !elems ) { 525 | return; 526 | } 527 | 528 | this.stamps = this.stamps.concat( elems ); 529 | // ignore 530 | elems.forEach( this.ignore, this ); 531 | }; 532 | 533 | /** 534 | * removes elements to stamps 535 | * @param {NodeList, Array, or Element} elems 536 | */ 537 | proto.unstamp = function( elems ) { 538 | elems = this._find( elems ); 539 | if ( !elems ){ 540 | return; 541 | } 542 | 543 | elems.forEach( function( elem ) { 544 | // filter out removed stamp elements 545 | utils.removeFrom( this.stamps, elem ); 546 | this.unignore( elem ); 547 | }, this ); 548 | }; 549 | 550 | /** 551 | * finds child elements 552 | * @param {NodeList, Array, Element, or String} elems 553 | * @returns {Array} elems 554 | */ 555 | proto._find = function( elems ) { 556 | if ( !elems ) { 557 | return; 558 | } 559 | // if string, use argument as selector string 560 | if ( typeof elems == 'string' ) { 561 | elems = this.element.querySelectorAll( elems ); 562 | } 563 | elems = utils.makeArray( elems ); 564 | return elems; 565 | }; 566 | 567 | proto._manageStamps = function() { 568 | if ( !this.stamps || !this.stamps.length ) { 569 | return; 570 | } 571 | 572 | this._getBoundingRect(); 573 | 574 | this.stamps.forEach( this._manageStamp, this ); 575 | }; 576 | 577 | // update boundingLeft / Top 578 | proto._getBoundingRect = function() { 579 | // get bounding rect for container element 580 | var boundingRect = this.element.getBoundingClientRect(); 581 | var size = this.size; 582 | this._boundingRect = { 583 | left: boundingRect.left + size.paddingLeft + size.borderLeftWidth, 584 | top: boundingRect.top + size.paddingTop + size.borderTopWidth, 585 | right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ), 586 | bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth ) 587 | }; 588 | }; 589 | 590 | /** 591 | * @param {Element} stamp 592 | **/ 593 | proto._manageStamp = noop; 594 | 595 | /** 596 | * get x/y position of element relative to container element 597 | * @param {Element} elem 598 | * @returns {Object} offset - has left, top, right, bottom 599 | */ 600 | proto._getElementOffset = function( elem ) { 601 | var boundingRect = elem.getBoundingClientRect(); 602 | var thisRect = this._boundingRect; 603 | var size = getSize( elem ); 604 | var offset = { 605 | left: boundingRect.left - thisRect.left - size.marginLeft, 606 | top: boundingRect.top - thisRect.top - size.marginTop, 607 | right: thisRect.right - boundingRect.right - size.marginRight, 608 | bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom 609 | }; 610 | return offset; 611 | }; 612 | 613 | // -------------------------- resize -------------------------- // 614 | 615 | // enable event handlers for listeners 616 | // i.e. resize -> onresize 617 | proto.handleEvent = utils.handleEvent; 618 | 619 | /** 620 | * Bind layout to window resizing 621 | */ 622 | proto.bindResize = function() { 623 | window.addEventListener( 'resize', this ); 624 | this.isResizeBound = true; 625 | }; 626 | 627 | /** 628 | * Unbind layout to window resizing 629 | */ 630 | proto.unbindResize = function() { 631 | window.removeEventListener( 'resize', this ); 632 | this.isResizeBound = false; 633 | }; 634 | 635 | proto.onresize = function() { 636 | this.resize(); 637 | }; 638 | 639 | utils.debounceMethod( Outlayer, 'onresize', 100 ); 640 | 641 | proto.resize = function() { 642 | // don't trigger if size did not change 643 | // or if resize was unbound. See #9 644 | if ( !this.isResizeBound || !this.needsResizeLayout() ) { 645 | return; 646 | } 647 | 648 | this.layout(); 649 | }; 650 | 651 | /** 652 | * check if layout is needed post layout 653 | * @returns Boolean 654 | */ 655 | proto.needsResizeLayout = function() { 656 | var size = getSize( this.element ); 657 | // check that this.size and size are there 658 | // IE8 triggers resize on body size change, so they might not be 659 | var hasSizes = this.size && size; 660 | return hasSizes && size.innerWidth !== this.size.innerWidth; 661 | }; 662 | 663 | // -------------------------- methods -------------------------- // 664 | 665 | /** 666 | * add items to Outlayer instance 667 | * @param {Array or NodeList or Element} elems 668 | * @returns {Array} items - Outlayer.Items 669 | **/ 670 | proto.addItems = function( elems ) { 671 | var items = this._itemize( elems ); 672 | // add items to collection 673 | if ( items.length ) { 674 | this.items = this.items.concat( items ); 675 | } 676 | return items; 677 | }; 678 | 679 | /** 680 | * Layout newly-appended item elements 681 | * @param {Array or NodeList or Element} elems 682 | */ 683 | proto.appended = function( elems ) { 684 | var items = this.addItems( elems ); 685 | if ( !items.length ) { 686 | return; 687 | } 688 | // layout and reveal just the new items 689 | this.layoutItems( items, true ); 690 | this.reveal( items ); 691 | }; 692 | 693 | /** 694 | * Layout prepended elements 695 | * @param {Array or NodeList or Element} elems 696 | */ 697 | proto.prepended = function( elems ) { 698 | var items = this._itemize( elems ); 699 | if ( !items.length ) { 700 | return; 701 | } 702 | // add items to beginning of collection 703 | var previousItems = this.items.slice(0); 704 | this.items = items.concat( previousItems ); 705 | // start new layout 706 | this._resetLayout(); 707 | this._manageStamps(); 708 | // layout new stuff without transition 709 | this.layoutItems( items, true ); 710 | this.reveal( items ); 711 | // layout previous items 712 | this.layoutItems( previousItems ); 713 | }; 714 | 715 | /** 716 | * reveal a collection of items 717 | * @param {Array of Outlayer.Items} items 718 | */ 719 | proto.reveal = function( items ) { 720 | this._emitCompleteOnItems( 'reveal', items ); 721 | if ( !items || !items.length ) { 722 | return; 723 | } 724 | var stagger = this.updateStagger(); 725 | items.forEach( function( item, i ) { 726 | item.stagger( i * stagger ); 727 | item.reveal(); 728 | }); 729 | }; 730 | 731 | /** 732 | * hide a collection of items 733 | * @param {Array of Outlayer.Items} items 734 | */ 735 | proto.hide = function( items ) { 736 | this._emitCompleteOnItems( 'hide', items ); 737 | if ( !items || !items.length ) { 738 | return; 739 | } 740 | var stagger = this.updateStagger(); 741 | items.forEach( function( item, i ) { 742 | item.stagger( i * stagger ); 743 | item.hide(); 744 | }); 745 | }; 746 | 747 | /** 748 | * reveal item elements 749 | * @param {Array}, {Element}, {NodeList} items 750 | */ 751 | proto.revealItemElements = function( elems ) { 752 | var items = this.getItems( elems ); 753 | this.reveal( items ); 754 | }; 755 | 756 | /** 757 | * hide item elements 758 | * @param {Array}, {Element}, {NodeList} items 759 | */ 760 | proto.hideItemElements = function( elems ) { 761 | var items = this.getItems( elems ); 762 | this.hide( items ); 763 | }; 764 | 765 | /** 766 | * get Outlayer.Item, given an Element 767 | * @param {Element} elem 768 | * @param {Function} callback 769 | * @returns {Outlayer.Item} item 770 | */ 771 | proto.getItem = function( elem ) { 772 | // loop through items to get the one that matches 773 | for ( var i=0; i < this.items.length; i++ ) { 774 | var item = this.items[i]; 775 | if ( item.element == elem ) { 776 | // return item 777 | return item; 778 | } 779 | } 780 | }; 781 | 782 | /** 783 | * get collection of Outlayer.Items, given Elements 784 | * @param {Array} elems 785 | * @returns {Array} items - Outlayer.Items 786 | */ 787 | proto.getItems = function( elems ) { 788 | elems = utils.makeArray( elems ); 789 | var items = []; 790 | elems.forEach( function( elem ) { 791 | var item = this.getItem( elem ); 792 | if ( item ) { 793 | items.push( item ); 794 | } 795 | }, this ); 796 | 797 | return items; 798 | }; 799 | 800 | /** 801 | * remove element(s) from instance and DOM 802 | * @param {Array or NodeList or Element} elems 803 | */ 804 | proto.remove = function( elems ) { 805 | var removeItems = this.getItems( elems ); 806 | 807 | this._emitCompleteOnItems( 'remove', removeItems ); 808 | 809 | // bail if no items to remove 810 | if ( !removeItems || !removeItems.length ) { 811 | return; 812 | } 813 | 814 | removeItems.forEach( function( item ) { 815 | item.remove(); 816 | // remove item from collection 817 | utils.removeFrom( this.items, item ); 818 | }, this ); 819 | }; 820 | 821 | // ----- destroy ----- // 822 | 823 | // remove and disable Outlayer instance 824 | proto.destroy = function() { 825 | // clean up dynamic styles 826 | var style = this.element.style; 827 | style.height = ''; 828 | style.position = ''; 829 | style.width = ''; 830 | // destroy items 831 | this.items.forEach( function( item ) { 832 | item.destroy(); 833 | }); 834 | 835 | this.unbindResize(); 836 | 837 | var id = this.element.outlayerGUID; 838 | delete instances[ id ]; // remove reference to instance by id 839 | delete this.element.outlayerGUID; 840 | // remove data for jQuery 841 | if ( jQuery ) { 842 | jQuery.removeData( this.element, this.constructor.namespace ); 843 | } 844 | 845 | }; 846 | 847 | // -------------------------- data -------------------------- // 848 | 849 | /** 850 | * get Outlayer instance from element 851 | * @param {Element} elem 852 | * @returns {Outlayer} 853 | */ 854 | Outlayer.data = function( elem ) { 855 | elem = utils.getQueryElement( elem ); 856 | var id = elem && elem.outlayerGUID; 857 | return id && instances[ id ]; 858 | }; 859 | 860 | 861 | // -------------------------- create Outlayer class -------------------------- // 862 | 863 | /** 864 | * create a layout class 865 | * @param {String} namespace 866 | */ 867 | Outlayer.create = function( namespace, options ) { 868 | // sub-class Outlayer 869 | var Layout = subclass( Outlayer ); 870 | // apply new options and compatOptions 871 | Layout.defaults = utils.extend( {}, Outlayer.defaults ); 872 | utils.extend( Layout.defaults, options ); 873 | Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions ); 874 | 875 | Layout.namespace = namespace; 876 | 877 | Layout.data = Outlayer.data; 878 | 879 | // sub-class Item 880 | Layout.Item = subclass( Item ); 881 | 882 | // -------------------------- declarative -------------------------- // 883 | 884 | utils.htmlInit( Layout, namespace ); 885 | 886 | // -------------------------- jQuery bridge -------------------------- // 887 | 888 | // make into jQuery plugin 889 | if ( jQuery && jQuery.bridget ) { 890 | jQuery.bridget( namespace, Layout ); 891 | } 892 | 893 | return Layout; 894 | }; 895 | 896 | function subclass( Parent ) { 897 | function SubClass() { 898 | Parent.apply( this, arguments ); 899 | } 900 | 901 | SubClass.prototype = Object.create( Parent.prototype ); 902 | SubClass.prototype.constructor = SubClass; 903 | 904 | return SubClass; 905 | } 906 | 907 | // ----- helpers ----- // 908 | 909 | // how many milliseconds are in each unit 910 | var msUnits = { 911 | ms: 1, 912 | s: 1000 913 | }; 914 | 915 | // munge time-like parameter into millisecond number 916 | // '0.4s' -> 40 917 | function getMilliseconds( time ) { 918 | if ( typeof time == 'number' ) { 919 | return time; 920 | } 921 | var matches = time.match( /(^\d*\.?\d*)(\w*)/ ); 922 | var num = matches && matches[1]; 923 | var unit = matches && matches[2]; 924 | if ( !num.length ) { 925 | return 0; 926 | } 927 | num = parseFloat( num ); 928 | var mult = msUnits[ unit ] || 1; 929 | return num * mult; 930 | } 931 | 932 | // ----- fin ----- // 933 | 934 | // back in global 935 | Outlayer.Item = Item; 936 | 937 | return Outlayer; 938 | 939 | })); 940 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outlayer", 3 | "version": "2.1.1", 4 | "description": "the brains and guts of a layout library", 5 | "main": "outlayer.js", 6 | "dependencies": { 7 | "ev-emitter": "^1.0.0", 8 | "get-size": "^2.0.2", 9 | "fizzy-ui-utils": "^2.0.0" 10 | }, 11 | "devDependencies": { 12 | "jquery": ">=1.4.3 <4", 13 | "jquery-bridget": "2.x", 14 | "qunitjs": "^2.0.0" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/metafizzy/outlayer.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/metafizzy/outlayer/issues" 22 | }, 23 | "homepage": "https://github.com/metafizzy/outlayer", 24 | "directories": { 25 | "doc": "docs", 26 | "example": "examples", 27 | "test": "test" 28 | }, 29 | "scripts": { 30 | "test": "echo \"Error: no test specified\" && exit 1" 31 | }, 32 | "keywords": [ 33 | "DOM", 34 | "layout" 35 | ], 36 | "author": "David DeSandro", 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /sandbox/browserify/browserify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | browserify 7 | 8 | 9 | 10 | 11 | 12 | 13 |

browserify

14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sandbox/browserify/main.js: -------------------------------------------------------------------------------- 1 | var CellsByRow = require('../cells-by-row'); 2 | 3 | new CellsByRow('#basic'); 4 | -------------------------------------------------------------------------------- /sandbox/cells-by-row.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CellsByRow 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 |

CellsByRow

19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /sandbox/cells-by-row.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CellsByRow example 3 | */ 4 | 5 | ( function( window, factory ) { 6 | /* jshint strict: false */ /* globals define, module, require */ 7 | if ( typeof define == 'function' && define.amd ) { 8 | // AMD 9 | define( [ 10 | '../outlayer' 11 | ], 12 | factory ); 13 | } else if ( typeof module == 'object' && module.exports ) { 14 | module.exports = factory( 15 | require('../outlayer') 16 | ); 17 | } else { 18 | // browser global 19 | window.CellsByRow = factory( 20 | window.Outlayer 21 | ); 22 | } 23 | 24 | }( window, function factory( Outlayer) { 25 | 'use strict'; 26 | 27 | var CellsByRow = Outlayer.create( 'cellsByRow', { 28 | columnWidth: 100, 29 | rowHeight: 100 30 | }); 31 | 32 | CellsByRow.prototype._resetLayout = function() { 33 | this.getSize(); 34 | 35 | this._getMeasurement( 'columnWidth', 'outerWidth' ); 36 | this._getMeasurement( 'rowHeight', 'outerHeight' ); 37 | 38 | var isHorizontal = this._getOption('horizontal'); 39 | if ( isHorizontal ) { 40 | this.rows = Math.floor( this.size.innerHeight / this.rowHeight ); 41 | this.rows = Math.max( this.rows, 1 ); 42 | } else { 43 | this.cols = Math.floor( this.size.innerWidth / this.columnWidth ); 44 | this.cols = Math.max( this.cols, 1 ); 45 | } 46 | 47 | this.itemIndex = 0; 48 | }; 49 | 50 | CellsByRow.prototype._getItemLayoutPosition = function( item ) { 51 | item.getSize(); 52 | var column, row; 53 | 54 | var isHorizontal = this._getOption('horizontal'); 55 | if ( isHorizontal ) { 56 | row = this.itemIndex % this.rows; 57 | column = Math.floor( this.itemIndex / this.rows ); 58 | } else { 59 | column = this.itemIndex % this.cols; 60 | row = Math.floor( this.itemIndex / this.cols ); 61 | } 62 | var x = column * this.columnWidth; 63 | var y = row * this.rowHeight; 64 | this.itemIndex++; 65 | return { 66 | x: x, 67 | y: y 68 | }; 69 | }; 70 | 71 | CellsByRow.prototype._getContainerSize = function() { 72 | var isHorizontal = this._getOption('horizontal'); 73 | if ( isHorizontal ) { 74 | return { 75 | width: Math.ceil( this.itemIndex / this.rows ) * this.columnWidth 76 | }; 77 | } else { 78 | return { 79 | height: Math.ceil( this.itemIndex / this.cols ) * this.rowHeight 80 | }; 81 | } 82 | }; 83 | 84 | return CellsByRow; 85 | 86 | })); 87 | -------------------------------------------------------------------------------- /sandbox/examples.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: sans-serif; 9 | } 10 | 11 | .container { 12 | background: #EEE; 13 | width: 50%; 14 | margin-bottom: 1.0em; 15 | } 16 | 17 | .item { 18 | border: 1px solid; 19 | background: #09F; 20 | width: 50px; 21 | height: 50px; 22 | } 23 | -------------------------------------------------------------------------------- /sandbox/fit-rows.js: -------------------------------------------------------------------------------- 1 | /*globals Outlayer */ 2 | 3 | ( function() { 4 | 'use strict'; 5 | 6 | var FitRows = window.FitRows = Outlayer.create('fitRows'); 7 | 8 | var proto = FitRows.prototype; 9 | 10 | proto._resetLayout = function() { 11 | this.getSize(); 12 | this.x = 0; 13 | this.y = 0; 14 | this.maxY = 0; 15 | this._getMeasurement( 'gutter', 'outerWidth' ); 16 | }; 17 | 18 | proto._getItemLayoutPosition = function( item ) { 19 | item.getSize(); 20 | 21 | var itemWidth = item.size.outerWidth + this.gutter; 22 | // if this element cannot fit in the current row 23 | var containerWidth = this.size.innerWidth + this.gutter; 24 | if ( this.x !== 0 && itemWidth + this.x > containerWidth ) { 25 | this.x = 0; 26 | this.y = this.maxY; 27 | } 28 | 29 | var position = { 30 | x: this.x, 31 | y: this.y 32 | }; 33 | 34 | this.maxY = Math.max( this.maxY, this.y + item.size.outerHeight ); 35 | this.x += itemWidth; 36 | 37 | return position; 38 | }; 39 | 40 | proto._getContainerSize = function() { 41 | return { height: this.maxY }; 42 | }; 43 | 44 | })(); 45 | -------------------------------------------------------------------------------- /sandbox/item-methods.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Item Methods 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Item Methods

14 | 15 |

16 | 17 | 18 | 19 |

20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /sandbox/padding-percent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | padding percent 8 | 9 | 57 | 58 | 59 | 60 | 61 |

padding + percent width bug

62 | 63 |

64 | 65 |
66 |
67 |
68 | 71 | 90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /sandbox/requirejs/main.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | baseUrl: '../../bower_components' 3 | // OR 4 | // paths: { 5 | // 'ev-emitter': 'bower_components/ev-emitter', 6 | // 'get-size': 'bower_components/get-size', 7 | // 'matches-selector': 'bower_components/matches-selector' 8 | // } 9 | }); 10 | 11 | requirejs( [ '../sandbox/cells-by-row' ], function( CellsByRow ) { 12 | new CellsByRow( document.querySelector('#basic') ); 13 | }); 14 | -------------------------------------------------------------------------------- /sandbox/requirejs/requirejs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | require js 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

require js

15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /sandbox/stagger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | stagger 8 | 9 | 24 | 25 | 26 | 27 | 28 |

stagger

29 | 30 |

31 | 32 |

33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /sandbox/toggler.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CellsByRow toggler 7 | 8 | 9 | 10 | 11 | 12 | 13 |

CellsByRow toggler

14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "devel": true, 4 | "undef": true, 5 | "unused": true, 6 | "globals": { 7 | "CellsByRow": false, 8 | "gimmeAnItemElement": true, 9 | "QUnit": false, 10 | "Outlayer": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | ( function() { 2 | 3 | window.gimmeAnItemElement = function() { 4 | var elem = document.createElement('div'); 5 | elem.className = 'item'; 6 | return elem; 7 | }; 8 | 9 | })(); 10 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Outlayer tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |

Outlayer tests

52 | 53 |
54 | 55 |
56 | 57 |

Defaults

58 | 59 |
60 |
61 |
62 | 63 |

Options

64 | 65 |
66 |
67 |
68 | 69 |

filter find

70 | 71 |
72 |
73 |
74 |
75 |
76 | 77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | 104 |

remove

105 | 106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | 119 |

Adding

120 | 121 |
122 |
123 |
124 |
125 | 126 |

Prepend

127 | 128 |
129 |
130 |
131 |
132 | 133 |

stamp

134 | 135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | 144 |

layout

145 | 146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | 155 |

percentPosition

156 | 157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | 166 |

hide/reveal

167 | 168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | 177 |

getMeasurements

178 | 179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | 189 |

_getElementOffset

190 | 191 |
192 |
193 |
194 |
195 | 196 | 201 | 202 |

origin

203 | 204 |
205 |
206 |
207 |
208 |
209 | 210 |

transitionDuration

211 | 212 |
213 |
214 |
215 |
216 |
217 |
218 | 219 |

destroy

220 | 221 |
222 |
223 |
224 |
225 |
226 | 227 |

Declarative

228 | 229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 | 238 | 239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 | 248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 | 257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 | 266 | 267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 | 276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 | 285 |

container size

286 | 287 |
288 | 289 |

jQuery plugin

290 | 291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /test/tests.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | .container { 8 | background: #EEE; 9 | width: 200px; 10 | margin-bottom: 1.0em; 11 | } 12 | 13 | .item { 14 | border: 1px solid; 15 | background: #09F; 16 | width: 50px; 17 | height: 50px; 18 | } 19 | 20 | .stamp { 21 | background: red; 22 | border: 1px solid; 23 | } 24 | 25 | #get-measurements .grid-sizer { 26 | width: 75px; 27 | height: 90px; 28 | } 29 | 30 | #offset { 31 | width: 300px; 32 | border-style: solid; 33 | border-color: #654; 34 | border-width: 0; 35 | /* border-width: 40px 30px 20px 10px;*/ 36 | /* padding: 10px 20px 30px 40px;*/ 37 | } 38 | 39 | #offset .stamp { 40 | position: absolute; 41 | border: 1px solid; 42 | } 43 | 44 | #offset .stamp1 { 45 | width: 100px; 46 | height: 50px; 47 | left: 0px; 48 | top: 0px; 49 | } 50 | 51 | #offset .stamp2 { 52 | width: 80px; 53 | height: 60px; 54 | right: 0px; 55 | bottom: 0px; 56 | } 57 | -------------------------------------------------------------------------------- /test/unit/add-items.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'addItems', function( assert ) { 2 | 3 | 4 | var olayer = new Outlayer( '#add-items', { 5 | itemSelector: '.item' 6 | }); 7 | 8 | var elem = gimmeAnItemElement(); 9 | var expectedItemCount = olayer.items.length; 10 | var items = olayer.addItems( elem ); 11 | 12 | assert.equal( items.length, 1, 'method return array of 1' ); 13 | assert.equal( olayer.items[2].element, elem, 'item was added, element matches' ); 14 | assert.equal( items[0] instanceof Outlayer.Item, true, 'item is instance of Outlayer.Item' ); 15 | expectedItemCount += 1; 16 | assert.equal( olayer.items.length, expectedItemCount, 'item added to items' ); 17 | 18 | // try it with an array 19 | var elems = [ gimmeAnItemElement(), gimmeAnItemElement(), document.createElement('div') ]; 20 | items = olayer.addItems( elems ); 21 | assert.equal( items.length, 2, 'method return array of 2' ); 22 | assert.equal( olayer.items[3].element, elems[0], 'item was added, element matches' ); 23 | expectedItemCount += 2; 24 | assert.equal( olayer.items.length, expectedItemCount, 'two items added to items' ); 25 | 26 | // try it with HTMLCollection / NodeList 27 | var fragment = document.createDocumentFragment(); 28 | fragment.appendChild( gimmeAnItemElement() ); 29 | fragment.appendChild( document.createElement('div') ); 30 | fragment.appendChild( gimmeAnItemElement() ); 31 | 32 | var divs = fragment.querySelectorAll('div'); 33 | items = olayer.addItems( divs ); 34 | assert.equal( items.length, 2, 'method return array of 2' ); 35 | assert.equal( olayer.items[5].element, divs[0], 'item was added, element matches' ); 36 | expectedItemCount += 2; 37 | assert.equal( olayer.items.length, expectedItemCount, 'two items added to items' ); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/basics.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'basics', function( assert ) { 2 | assert.equal( typeof Outlayer, 'function', 'Outlayer is a function' ); 3 | // TODO pckry should be null or something 4 | // var olayer = new Outlayer(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/unit/container-size.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'container size', function( assert ) { 2 | 3 | // test layout that just sets size 4 | var Sizer = Outlayer.create( 'sizer', { 5 | width: 220, 6 | height: 120 7 | }); 8 | 9 | Sizer.prototype._getContainerSize = function() { 10 | return { 11 | width: this.options.width, 12 | height: this.options.height 13 | }; 14 | }; 15 | 16 | var elem = document.querySelector('#container-size'); 17 | 18 | var layout = new Sizer( elem ); 19 | 20 | function checkSize( width, height ) { 21 | assert.equal( elem.style.width, width + 'px', 'width = ' + width ); 22 | assert.equal( elem.style.height, height + 'px', 'height = ' + height ); 23 | } 24 | 25 | checkSize( 220, 120 ); 26 | 27 | // disable resizing 28 | layout.options.resizeContainer = false; 29 | layout.options.width = 180; 30 | layout.options.height = 230; 31 | layout.layout(); 32 | checkSize( 220, 120 ); 33 | layout.options.resizeContainer = true; 34 | layout.options.width = 220; 35 | layout.options.height = 120; 36 | 37 | if ( layout.size.isBorderBox ) { 38 | elem.style.padding = '10px 20px 30px 40px'; 39 | layout.layout(); 40 | checkSize( 280, 160 ); 41 | 42 | elem.style.borderStyle = 'solid'; 43 | elem.style.borderWidth = '4px 3px 2px 1px'; 44 | 45 | layout.layout(); 46 | checkSize( 284, 166 ); 47 | } 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/create.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'create Layouts', function( assert ) { 2 | 3 | var Leiout = Outlayer.create('leiout'); 4 | Leiout.Item.prototype.foo = 'bar'; 5 | Leiout.compatOptions.fitWidth = 'isFitWidth'; 6 | var elem = document.createElement('div'); 7 | var lei = new Leiout( elem, { 8 | isFitWidth: 300 9 | }); 10 | var outlayr = new Outlayer( elem ); 11 | 12 | assert.equal( typeof CellsByRow, 'function', 'CellsByRow is a function' ); 13 | assert.equal( CellsByRow.namespace, 'cellsByRow', 'cellsByRow namespace' ); 14 | assert.equal( Outlayer.namespace, 'outlayer', 'Outlayer namespace unchanged' ); 15 | assert.equal( Leiout.namespace, 'leiout', 'Leiout namespace' ); 16 | assert.equal( CellsByRow.defaults.resize, true, 'resize option there' ); 17 | assert.equal( CellsByRow.defaults.columnWidth, 100, 'columnWidth option set' ); 18 | assert.strictEqual( Outlayer.defaults.columnWidth, undefined, 'Outlayer has no default columnWidth' ); 19 | assert.strictEqual( Leiout.defaults.columnWidth, undefined, 'Leiout has no default columnWidth' ); 20 | assert.equal( lei.constructor.Item, Leiout.Item, 'Leiout.Item is on constructor.Item' ); 21 | assert.equal( lei._getOption('fitWidth'), 300, 'backwards compatible _getOption' ); 22 | assert.equal( outlayr.constructor.Item, Outlayer.Item, 'outlayr.Item is still correct Item' ); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/declarative.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'declarative', function( assert ) { 2 | var $ = window.jQuery; 3 | 4 | // data-cells-by-row, no data-cells-by-row-options 5 | ( function() { 6 | var container = document.querySelector('#declarative-attr'); 7 | var cellsLayout = CellsByRow.data( container ); 8 | assert.ok( cellsLayout instanceof CellsByRow, '.data() works, retrieves instance' ); 9 | assert.deepEqual( cellsLayout.options, CellsByRow.defaults, 'options match defaults' ); 10 | assert.ok( cellsLayout._isLayoutInited, 'cellsLayout._isLayoutInited' ); 11 | var itemElem = cellsLayout.items[0].element; 12 | assert.equal( itemElem.style.left, '0px', 'first item style left set' ); 13 | assert.equal( itemElem.style.top, '0px', 'first item style top set' ); 14 | })(); 15 | 16 | // data-cells-by-row, has data-cells-by-row-options, but bad JSON 17 | ( function() { 18 | var container = document.querySelector('#declarative-attr-bad-json'); 19 | var cellsLayout = CellsByRow.data( container ); 20 | assert.ok( !cellsLayout, 'bad JSON in data-cells-by-row-options does not init CellsByRow' ); 21 | assert.ok( !container.outlayerGUID, 'no expando property on element' ); 22 | })(); 23 | 24 | // data-cells-by-row, has good data-packery-options 25 | ( function() { 26 | var container = document.querySelector('#declarative-attr-good-json'); 27 | var cellsLayout = CellsByRow.data( container ); 28 | assert.ok( cellsLayout instanceof CellsByRow, '.data() got CellByRow instance retrieved from element, with good JSON in data-cells-by-row-options' ); 29 | assert.strictEqual( cellsLayout.options.columnWidth, 25, 'columnWidth option was set' ); 30 | assert.strictEqual( cellsLayout.options.rowHeight, 30, 'rowHeight option was set' ); 31 | assert.strictEqual( cellsLayout.options.isResizable, false, 'isResizable option was set' ); 32 | assert.strictEqual( cellsLayout.options.foo, 'bar', 'foo option was set' ); 33 | 34 | assert.equal( $.data( container, 'cellsByRow' ), cellsLayout, 'jQuery.data( elem, "cellsByRow") returns CellsByRow instance' ); 35 | })(); 36 | 37 | // js-cells-by-row, no data-cells-by-row-options 38 | ( function() { 39 | var container = document.querySelector('#declarative-js-class'); 40 | var cellsLayout = CellsByRow.data( container ); 41 | assert.ok( cellsLayout instanceof CellsByRow, '.data() works, retrieves instance' ); 42 | assert.deepEqual( cellsLayout.options, CellsByRow.defaults, 'options match defaults' ); 43 | assert.ok( cellsLayout._isLayoutInited, 'cellsLayout._isLayoutInited' ); 44 | var itemElem = cellsLayout.items[0].element; 45 | assert.equal( itemElem.style.left, '0px', 'first item style left set' ); 46 | assert.equal( itemElem.style.top, '0px', 'first item style top set' ); 47 | })(); 48 | 49 | // js-cells-by-row, has data-cells-by-row-options, but bad JSON 50 | ( function() { 51 | var container = document.querySelector('#declarative-js-class-bad-json'); 52 | var cellsLayout = CellsByRow.data( container ); 53 | assert.ok( !cellsLayout, 'bad JSON in data-cells-by-row-options does not init CellsByRow' ); 54 | assert.ok( !container.outlayerGUID, 'no expando property on element' ); 55 | })(); 56 | 57 | // js-cells-by-row, has good data-packery-options 58 | ( function() { 59 | var container = document.querySelector('#declarative-js-class-good-json'); 60 | var cellsLayout = CellsByRow.data( container ); 61 | assert.ok( cellsLayout instanceof CellsByRow, '.data() got CellByRow instance retrieved from element, with good JSON in data-cells-by-row-options' ); 62 | assert.strictEqual( cellsLayout.options.columnWidth, 25, 'columnWidth option was set' ); 63 | assert.strictEqual( cellsLayout.options.rowHeight, 30, 'rowHeight option was set' ); 64 | assert.strictEqual( cellsLayout.options.isResizable, false, 'isResizable option was set' ); 65 | assert.strictEqual( cellsLayout.options.foo, 'bar', 'foo option was set' ); 66 | 67 | assert.equal( $.data( container, 'cellsByRow' ), cellsLayout, 'jQuery.data( elem, "cellsByRow") returns CellsByRow instance' ); 68 | })(); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /test/unit/defaults.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'defaults', function( assert ) { 2 | var container = document.querySelector('#defaults'); 3 | var olayer = new Outlayer( container ); 4 | var item = olayer.items[0]; 5 | assert.deepEqual( olayer.options, Outlayer.defaults, 'default options match prototype' ); 6 | assert.equal( typeof olayer.items, 'object', 'items is object' ); 7 | assert.equal( olayer.items.length, 1, 'one item' ); 8 | assert.equal( Outlayer.data( container ), olayer, 'data method returns instance' ); 9 | assert.ok( olayer.resize, 'resize' ); 10 | 11 | assert.deepEqual( item.options, Outlayer.Item.prototype.options, 'default item options match Outlayer.Item' ); 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /test/unit/destroy.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'destroy', function( assert ) { 2 | 3 | var container = document.querySelector('#destroy'); 4 | var layout = new CellsByRow( container ); 5 | 6 | layout.destroy(); 7 | 8 | assert.ok( !CellsByRow.data( container ), '.data() returns falsey' ); 9 | 10 | function checkStyle( elem, property ) { 11 | assert.ok( !elem.style[ property ], elem + ' has no ' + property + ' style' ); 12 | } 13 | 14 | checkStyle( container, 'height' ); 15 | checkStyle( container, 'position' ); 16 | 17 | var items = container.querySelectorAll('.item'); 18 | for ( var i=0, len = items.length; i < len; i++ ) { 19 | var itemElem = items[i]; 20 | checkStyle( itemElem, 'position' ); 21 | checkStyle( itemElem, 'left' ); 22 | checkStyle( itemElem, 'top' ); 23 | } 24 | 25 | // try to force a resize 26 | container.style.width = '300px'; 27 | layout.resize(); 28 | 29 | checkStyle( container, 'height' ); 30 | checkStyle( container, 'position' ); 31 | checkStyle( items[0], 'position' ); 32 | checkStyle( items[0], 'left' ); 33 | checkStyle( items[0], 'top' ); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/filter-find.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'filter find item elements', function( assert ) { 2 | 3 | ( function() { 4 | var olayer = new Outlayer( '#children' ); 5 | assert.equal( olayer.items.length, 3, 'no itemSeletor, gets all children' ); 6 | })(); 7 | 8 | ( function() { 9 | var olayer = new Outlayer( '#filtered', { 10 | itemSelector: '.item' 11 | }); 12 | assert.equal( olayer.items.length, 6, 'filtered, itemSelector = .item, not all children' ); 13 | })(); 14 | 15 | ( function() { 16 | var olayer = new Outlayer( '#found', { 17 | itemSelector: '.item' 18 | }); 19 | assert.equal( olayer.items.length, 4, 'found itemSelector = .item, querySelectoring' ); 20 | })(); 21 | 22 | ( function() { 23 | var olayer = new Outlayer( '#filter-found', { 24 | itemSelector: '.item' 25 | }); 26 | assert.equal( olayer.items.length, 5, 'filter found' ); 27 | })(); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /test/unit/get-measurements.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'getMeasurements', function( assert ) { 2 | var container = document.querySelector('#get-measurements'); 3 | var layout = new CellsByRow( container, { 4 | itemSelector: '.item', 5 | columnWidth: 80, 6 | rowHeight: 65 7 | }); 8 | 9 | assert.equal( layout.columnWidth, 80, 'columnWidth option set 80' ); 10 | assert.equal( layout.rowHeight, 65, 'rowHeight option set 65' ); 11 | 12 | var gridSizer = container.querySelector('.grid-sizer'); 13 | 14 | layout.options.columnWidth = gridSizer; 15 | layout.options.rowHeight = gridSizer; 16 | layout.layout(); 17 | 18 | assert.equal( layout.columnWidth, 75, 'columnWidth element sized as 75px' ); 19 | assert.equal( layout.rowHeight, 90, 'rowHeight element sized as 90px' ); 20 | 21 | gridSizer.style.width = '50%'; 22 | layout.layout(); 23 | 24 | assert.equal( layout.columnWidth, 100, 'columnWidth element sized as 50% => 100px' ); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/unit/hide-reveal.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'hide/reveal', function( assert ) { 2 | 3 | var CellsByRow = window.CellsByRow; 4 | var gridElem = document.querySelector('#hide-reveal'); 5 | 6 | var layout = new CellsByRow( gridElem, { 7 | columnWidth: 60, 8 | rowHeight: 60, 9 | transitionDuration: '0.2s' 10 | }); 11 | 12 | var hideElems = gridElem.querySelectorAll('.hideable'); 13 | var hideItems = layout.getItems( hideElems ); 14 | var lastIndex = hideItems.length - 1; 15 | var firstItemElem = hideItems[0].element; 16 | var lastItemElem = hideItems[ lastIndex ].element; 17 | 18 | var done = assert.async(); 19 | 20 | layout.once( 'hideComplete', function( hideCompleteItems ) { 21 | assert.ok( true, 'hideComplete event did fire' ); 22 | assert.equal( hideCompleteItems.length, hideItems.length, 'event-emitted items matches layout items length' ); 23 | assert.strictEqual( hideCompleteItems[0], hideItems[0], 'event-emitted items has same first item' ); 24 | assert.strictEqual( hideCompleteItems[ lastIndex ], hideItems[ lastIndex ], 'event-emitted items has same last item' ); 25 | assert.equal( firstItemElem.style.display, 'none', 'first item hidden' ); 26 | assert.equal( lastItemElem.style.display, 'none', 'last item hidden' ); 27 | assert.equal( firstItemElem.style.opacity, '', 'first item opacity not set' ); 28 | assert.equal( lastItemElem.style.opacity, '', 'last item opacity not set' ); 29 | setTimeout( nextReveal ); 30 | }); 31 | 32 | layout.hide( hideItems ); 33 | 34 | // -------------------------- -------------------------- // 35 | 36 | function nextReveal() { 37 | layout.once( 'revealComplete', function( revealCompleteItems ) { 38 | assert.ok( true, 'revealComplete event did fire' ); 39 | assert.equal( revealCompleteItems.length, hideItems.length, 'event-emitted items matches layout items length' ); 40 | assert.strictEqual( revealCompleteItems[0], hideItems[0], 'event-emitted items has same first item' ); 41 | assert.strictEqual( revealCompleteItems[ lastIndex ], hideItems[ lastIndex ], 'event-emitted items has same last item' ); 42 | assert.equal( firstItemElem.style.display, '', 'first item no display' ); 43 | assert.equal( lastItemElem.style.display, '', 'last item no display' ); 44 | assert.equal( firstItemElem.style.opacity, '', 'first item opacity not set' ); 45 | assert.equal( lastItemElem.style.opacity, '', 'last item opacity not set' ); 46 | setTimeout( nextHideNoTransition ); 47 | }); 48 | 49 | layout.reveal( hideItems ); 50 | } 51 | 52 | // -------------------------- -------------------------- // 53 | 54 | function nextHideNoTransition() { 55 | layout.once( 'hideComplete', function( hideCompleteItems ) { 56 | assert.ok( true, 'hideComplete event did fire' ); 57 | assert.equal( hideCompleteItems.length, hideItems.length, 'event-emitted items matches layout items length' ); 58 | assert.strictEqual( hideCompleteItems[0], hideItems[0], 'event-emitted items has same first item' ); 59 | assert.strictEqual( hideCompleteItems[ lastIndex ], hideItems[ lastIndex ], 'event-emitted items has same last item' ); 60 | assert.equal( firstItemElem.style.display, 'none', 'first item hidden' ); 61 | assert.equal( lastItemElem.style.display, 'none', 'last item hidden' ); 62 | assert.equal( firstItemElem.style.opacity, '', 'first item opacity not set' ); 63 | assert.equal( lastItemElem.style.opacity, '', 'last item opacity not set' ); 64 | setTimeout( nextRevealNoTransition ); 65 | // start(); 66 | }); 67 | 68 | layout.transitionDuration = 0; 69 | layout.hide( hideItems ); 70 | } 71 | 72 | // -------------------------- -------------------------- // 73 | 74 | function nextRevealNoTransition() { 75 | layout.once( 'revealComplete', function( revealCompleteItems ) { 76 | assert.ok( true, 'revealComplete event did fire' ); 77 | assert.equal( revealCompleteItems.length, hideItems.length, 'event-emitted items matches layout items length' ); 78 | assert.strictEqual( revealCompleteItems[0], hideItems[0], 'event-emitted items has same first item' ); 79 | assert.strictEqual( revealCompleteItems[ lastIndex ], hideItems[ lastIndex ], 'event-emitted items has same last item' ); 80 | assert.equal( firstItemElem.style.display, '', 'first item no display' ); 81 | assert.equal( lastItemElem.style.display, '', 'last item no display' ); 82 | assert.equal( firstItemElem.style.opacity, '', 'first item opacity not set' ); 83 | assert.equal( lastItemElem.style.opacity, '', 'last item opacity not set' ); 84 | setTimeout( nextHideNone ); 85 | // start(); 86 | }); 87 | 88 | layout.reveal( hideItems ); 89 | } 90 | 91 | function nextHideNone() { 92 | var emptyArray = []; 93 | layout.once( 'hideComplete', function( hideCompleteItems ) { 94 | assert.ok( true, 'hideComplete event did fire with no items' ); 95 | assert.equal( hideCompleteItems, emptyArray, 'returns same object passed in' ); 96 | setTimeout( nextRevealNone ); 97 | // start(); 98 | }); 99 | 100 | layout.hide( emptyArray ); 101 | } 102 | 103 | function nextRevealNone() { 104 | var emptyArray = []; 105 | layout.once( 'revealComplete', function( revealCompleteItems ) { 106 | assert.ok( true, 'revealComplete event did fire with no items' ); 107 | assert.equal( revealCompleteItems, emptyArray, 'returns same object passed in' ); 108 | setTimeout( nextHideItemElements ); 109 | // start(); 110 | }); 111 | 112 | layout.reveal( emptyArray ); 113 | } 114 | 115 | // -------------------------- -------------------------- // 116 | 117 | function nextHideItemElements() { 118 | layout.once( 'hideComplete', function( hideCompleteItems ) { 119 | assert.ok( true, 'hideComplete event did fire after hideItemElements' ); 120 | assert.equal( hideCompleteItems.length, hideItems.length, 'event-emitted items matches layout items length' ); 121 | assert.strictEqual( hideCompleteItems[0], hideItems[0], 'event-emitted items has same first item' ); 122 | assert.strictEqual( hideCompleteItems[ lastIndex ], hideItems[ lastIndex ], 'event-emitted items has same last item' ); 123 | assert.equal( firstItemElem.style.display, 'none', 'first item hidden' ); 124 | assert.equal( lastItemElem.style.display, 'none', 'last item hidden' ); 125 | assert.equal( firstItemElem.style.opacity, '', 'first item opacity not set' ); 126 | assert.equal( lastItemElem.style.opacity, '', 'last item opacity not set' ); 127 | setTimeout( nextRevealItemElements ); 128 | // start(); 129 | }); 130 | 131 | layout.hideItemElements( hideElems ); 132 | } 133 | 134 | function nextRevealItemElements() { 135 | layout.once( 'revealComplete', function( revealCompleteItems ) { 136 | assert.ok( true, 'revealComplete event did fire after revealItemElements' ); 137 | assert.equal( revealCompleteItems.length, hideItems.length, 'event-emitted items matches layout items length' ); 138 | assert.strictEqual( revealCompleteItems[0], hideItems[0], 'event-emitted items has same first item' ); 139 | assert.strictEqual( revealCompleteItems[ lastIndex ], hideItems[ lastIndex ], 'event-emitted items has same last item' ); 140 | assert.equal( firstItemElem.style.display, '', 'first item no display' ); 141 | assert.equal( lastItemElem.style.display, '', 'last item no display' ); 142 | assert.equal( firstItemElem.style.opacity, '', 'first item opacity not set' ); 143 | assert.equal( lastItemElem.style.opacity, '', 'last item opacity not set' ); 144 | // setTimeout( nextHideNoTransition ); 145 | done(); 146 | }); 147 | 148 | layout.revealItemElements( hideElems ); 149 | } 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /test/unit/item-on-transition-end.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'item onTransitionEnd', function( assert ) { 2 | 3 | var container = document.querySelector('#item-on-transition-end'); 4 | var layout = new Outlayer( container, { 5 | containerStyle: { top: 0 }, 6 | transitionDuration: '1s' 7 | }); 8 | var item = layout.items[0]; 9 | item.addListener( 'transitionEnd', function() { 10 | console.log( item.element.style.display ); } ); 11 | // item.on( 'transitionEnd', function() { 12 | // console.log( item.element.style.display ); } ); 13 | // var itemElem = layout.items[0].element; 14 | var done = assert.async(); 15 | // hide, then immediate reveal again, while item is still transitioning 16 | layout.hide( [ item ] ); 17 | setTimeout( function() { 18 | item.addListener( 'transitionEnd', function() { 19 | console.log('second', item.element.style.display ); 20 | // console.log( item.element.style.display ); 21 | assert.ok( true, true ); 22 | // assert.equal( item.element.style.display, '', 'item was not hidden'); 23 | done(); 24 | }); 25 | layout.reveal( [ item ] ); 26 | }, 500 ); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/jquery-plugin.js: -------------------------------------------------------------------------------- 1 | 2 | QUnit.test( 'jQuery plugin', function( assert ) { 3 | var $ = window.jQuery; 4 | 5 | var $elem = $('#jquery'); 6 | assert.ok( $.fn.cellsByRow, '.cellsByRow is in jQuery.fn namespace' ); 7 | assert.equal( typeof $elem.cellsByRow, 'function', '.cellsByRow is a plugin' ); 8 | $elem.cellsByRow(); 9 | var layout = $elem.data('cellsByRow'); 10 | assert.ok( layout, 'CellsByRow instance via .data()' ); 11 | assert.equal( layout, CellsByRow.data( $elem[0] ), 'instance matches the same one via CellsByRow.data()' ); 12 | 13 | // destroy and re-init 14 | $elem.cellsByRow('destroy'); 15 | $elem.cellsByRow(); 16 | assert.notEqual( $elem.data('cellsByRow'), layout, 'new CellsByRow instance after destroy' ); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/unit/layout.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'layout', function( assert ) { 2 | 3 | var cellsLayout = new CellsByRow( document.querySelector('#layout') ); 4 | var items = cellsLayout.items; 5 | assert.ok( cellsLayout._isLayoutInited, '_isLayoutInited' ); 6 | 7 | var done = assert.async(); 8 | 9 | cellsLayout.once( 'layoutComplete', function onLayout( layoutItems ) { 10 | assert.ok( true, 'layoutComplete event did fire' ); 11 | assert.equal( layoutItems.length, items.length, 'event-emitted items matches layout items length' ); 12 | assert.strictEqual( layoutItems[0], items[0], 'event-emitted items has same first item' ); 13 | var len = layoutItems.length - 1; 14 | assert.strictEqual( layoutItems[ len ], items[ len ], 'event-emitted items has same last item' ); 15 | done(); 16 | }); 17 | 18 | cellsLayout.options.columnWidth = 60; 19 | cellsLayout.options.rowHeight = 60; 20 | cellsLayout.layout(); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/offset.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'offset', function( assert ) { 2 | 3 | var container = document.querySelector('#offset'); 4 | var stamp1 = container.querySelector('.stamp1'); 5 | var stamp2 = container.querySelector('.stamp2'); 6 | var layout = new Outlayer( container, { 7 | itemSelector: '.item' 8 | }); 9 | container.style.height = '300px'; 10 | 11 | layout.getSize(); 12 | layout._getBoundingRect(); 13 | var offset1 = layout._getElementOffset( stamp1 ); 14 | var offset2 = layout._getElementOffset( stamp2 ); 15 | // console.log( offset ); 16 | assert.equal( offset1.left, 0, 'stamp1 offset left: 0' ); 17 | assert.equal( offset1.top, 0, 'stamp1 offset top: 0' ); 18 | assert.equal( offset2.right, 0, 'stamp2 offset right: 0' ); 19 | assert.equal( offset2.bottom, 0, 'stamp2 offset bottom: 0' ); 20 | 21 | stamp1.style.left = '40px'; 22 | stamp1.style.top = '20px'; 23 | stamp2.style.right = '50px'; 24 | stamp2.style.bottom = '30px'; 25 | offset1 = layout._getElementOffset( stamp1 ); 26 | offset2 = layout._getElementOffset( stamp2 ); 27 | // console.log( offset ); 28 | assert.equal( offset1.left, 40, 'stamp1 offset left: 40' ); 29 | assert.equal( offset1.top, 20, 'stamp1 offset top: 20' ); 30 | assert.equal( offset2.right, 50, 'stamp2 offset right: 50' ); 31 | assert.equal( offset2.bottom, 30, 'stamp2 offset bottom: 30' ); 32 | 33 | // add border to container 34 | container.style.borderWidth = '40px 30px 20px 10px'; 35 | layout.getSize(); 36 | layout._getBoundingRect(); 37 | offset1 = layout._getElementOffset( stamp1 ); 38 | offset2 = layout._getElementOffset( stamp2 ); 39 | // left/top should still be the same 40 | assert.equal( offset1.left, 40, 'stamp1 offset with border left: 40' ); 41 | assert.equal( offset1.top, 20, 'stamp1 offset with border top: 20' ); 42 | assert.equal( offset2.right, 50, 'stamp2 offset with border right: 50' ); 43 | assert.equal( offset2.bottom, 30, 'stamp2 offset with border bottom: 30' ); 44 | // add padding to container 45 | container.style.padding = '10px 20px 30px 40px'; 46 | layout.getSize(); 47 | layout._getBoundingRect(); 48 | offset1 = layout._getElementOffset( stamp1 ); 49 | offset2 = layout._getElementOffset( stamp2 ); 50 | 51 | assert.equal( offset1.left, 0, 'stamp1 offset with border and padding, left: 0' ); 52 | assert.equal( offset1.top, 10, 'stamp1 offset with border and padding, top: 10' ); 53 | assert.equal( offset2.right, 30, 'stamp2 offset with border and padding, right: 30' ); 54 | assert.equal( offset2.bottom, 0, 'stamp2 offset with border and padding, bottom: 0' ); 55 | 56 | // add margin to stamps 57 | stamp1.style.margin = '5px 10px 15px 20px'; 58 | stamp2.style.margin = '5px 10px 15px 20px'; 59 | layout.getSize(); 60 | layout._getBoundingRect(); 61 | offset1 = layout._getElementOffset( stamp1 ); 62 | offset2 = layout._getElementOffset( stamp2 ); 63 | 64 | assert.equal( offset1.left, 0, 'stamp1 offset with margin, left: 0' ); 65 | assert.equal( offset1.top, 10, 'stamp1 offset with margin, top: 10' ); 66 | assert.equal( offset2.right, 30, 'stamp2 offset with margin, right: 30' ); 67 | assert.equal( offset2.bottom, 0, 'stamp2 offset with margin, bottom: 0' ); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/unit/options.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'options', function( assert ) { 2 | var container = document.querySelector('#options'); 3 | var olayer = new Outlayer( container, { 4 | initLayout: false, 5 | transitionDuration: '600ms' 6 | }); 7 | 8 | assert.ok( !olayer._isLayoutInited, 'olayer is not layout initialized' ); 9 | assert.equal( olayer.options.transitionDuration, '600ms', 'transition option set'); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /test/unit/origin.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'origin', function( assert ) { 2 | 3 | var elem = document.querySelector('#origin'); 4 | var layout = new CellsByRow( elem, { 5 | itemOptions: { 6 | transitionDuration: '0.1s' 7 | } 8 | }); 9 | 10 | function checkItemPosition( itemIndex, x, y ) { 11 | var itemElem = layout.items[ itemIndex ].element; 12 | var message = 'item ' + itemIndex + ' '; 13 | var xProperty = layout.options.originLeft ? 'left' : 'right'; 14 | var yProperty = layout.options.originTop ? 'top' : 'bottom'; 15 | assert.equal( itemElem.style[ xProperty ], x + 'px', message + xProperty + ' = ' + x ); 16 | assert.equal( itemElem.style[ yProperty ], y + 'px', message + yProperty + ' = ' + y ); 17 | } 18 | 19 | // top left 20 | checkItemPosition( 0, 0, 0 ); 21 | checkItemPosition( 1, 100, 0 ); 22 | checkItemPosition( 2, 0, 100 ); 23 | 24 | // top right 25 | layout.options.originLeft = false; 26 | layout.once( 'layoutComplete', function() { 27 | checkItemPosition( 0, 0, 0 ); 28 | checkItemPosition( 1, 100, 0 ); 29 | checkItemPosition( 2, 0, 100 ); 30 | setTimeout( testBottomRight ); 31 | // start(); 32 | }); 33 | 34 | var done = assert.async(); 35 | 36 | layout.layout(); 37 | 38 | // bottom right 39 | function testBottomRight() { 40 | layout.options.originTop = false; 41 | layout.once( 'layoutComplete', function() { 42 | checkItemPosition( 0, 0, 0 ); 43 | checkItemPosition( 1, 100, 0 ); 44 | checkItemPosition( 2, 0, 100 ); 45 | setTimeout( testBottomLeft ); 46 | }); 47 | layout.layout(); 48 | } 49 | 50 | // bottom right 51 | function testBottomLeft() { 52 | layout.options.originLeft = true; 53 | layout.once( 'layoutComplete', function() { 54 | checkItemPosition( 0, 0, 0 ); 55 | checkItemPosition( 1, 100, 0 ); 56 | checkItemPosition( 2, 0, 100 ); 57 | done(); 58 | }); 59 | layout.layout(); 60 | } 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/percent-position.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'percentPosition', function( assert ) { 2 | 3 | var gridElem = document.querySelector('#percent-position'); 4 | var layout = new CellsByRow( gridElem, { 5 | percentPosition: true, 6 | columnWidth: 50, 7 | rowHeight: 50, 8 | transitionDuration: 0 9 | }); 10 | 11 | var itemElems = gridElem.querySelectorAll('.item'); 12 | 13 | assert.equal( itemElems[0].style.left, '0%', 'first item left 0%' ); 14 | assert.equal( itemElems[1].style.left, '25%', '2nd item left 25%' ); 15 | assert.equal( itemElems[2].style.left, '50%', 'first item left 50%' ); 16 | assert.equal( itemElems[3].style.left, '75%', 'first item left 75%' ); 17 | 18 | // set top 19 | gridElem.style.height = '200px'; 20 | layout.options.horizontal = true; 21 | layout.layout(); 22 | 23 | assert.equal( itemElems[0].style.top, '0%', 'first item top 0%' ); 24 | assert.equal( itemElems[1].style.top, '25%', 'second item top 25%' ); 25 | assert.equal( itemElems[2].style.top, '50%', 'first item top 50%' ); 26 | assert.equal( itemElems[3].style.top, '75%', 'first item top 75%' ); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/prepend.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'prepend', function( assert ) { 2 | var container = document.querySelector('#prepend'); 3 | var olayer = new Outlayer( container ); 4 | var itemElemA = olayer.items[0].element; 5 | var itemElemB = olayer.items[1].element; 6 | var itemElemC = gimmeAnItemElement(); 7 | itemElemC.style.background = 'orange'; 8 | var itemElemD = gimmeAnItemElement(); 9 | itemElemD.style.background = 'magenta'; 10 | 11 | // TODO re-enable this, possible with CellsByRow 12 | // var ticks = 0; 13 | 14 | // olayer.on( 'layoutComplete', function() { 15 | // assert.ok( true, 'layoutComplete triggered' ); 16 | // ticks++; 17 | // if ( ticks == 2 ) { 18 | // assert.ok( true, '2 layoutCompletes triggered' ); 19 | // start(); 20 | // } 21 | // }); 22 | // 23 | // stop(); 24 | var fragment = document.createDocumentFragment(); 25 | fragment.appendChild( itemElemC ); 26 | fragment.appendChild( itemElemD ); 27 | container.insertBefore( fragment, container.firstChild ); 28 | olayer.prepended([ itemElemC, itemElemD ]); 29 | 30 | assert.equal( olayer.items[0].element, itemElemC, 'item C is first' ); 31 | assert.equal( olayer.items[1].element, itemElemD, 'item D is second' ); 32 | assert.equal( olayer.items[2].element, itemElemA, 'item A is third' ); 33 | assert.equal( olayer.items[3].element, itemElemB, 'item B is fourth' ); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/remove.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'remove', function( assert ) { 2 | var container = document.querySelector('#remove'); 3 | // packery starts with 4 items 4 | var olayer = new Outlayer( container, { 5 | itemSelector: '.item' 6 | }); 7 | // remove .w2 items 8 | var w2Elems = container.querySelectorAll('.w2'); 9 | var expectedRemovedCount = olayer.items.length - w2Elems.length; 10 | 11 | olayer.once( 'removeComplete', function( removedItems ) { 12 | assert.ok( true, 'removeComplete event did fire' ); 13 | assert.equal( removedItems.length, w2Elems.length, 'remove elems length matches 2nd argument length' ); 14 | for ( var i=0, len = removedItems.length; i < len; i++ ) { 15 | assert.equal( removedItems[i].element, w2Elems[i], 'removedItems element matches' ); 16 | } 17 | assert.equal( container.children.length, expectedRemovedCount, 'elements removed from DOM' ); 18 | assert.equal( container.querySelectorAll('.w2').length, 0, 'matched elements were removed' ); 19 | setTimeout( removeNoTransition, 20 ); 20 | // start(); 21 | }); 22 | 23 | var done = assert.async(); 24 | 25 | olayer.remove( w2Elems ); 26 | assert.equal( olayer.items.length, expectedRemovedCount, 'items removed from Packery instance' ); 27 | 28 | // check items are remove with no transition 29 | function removeNoTransition() { 30 | // disable transition by setting transition duration to 0 31 | olayer.options.transitionDuration = 0; 32 | var h2Elems = container.querySelectorAll('.h2'); 33 | expectedRemovedCount -= h2Elems.length; 34 | 35 | olayer.once( 'removeComplete', function( removedItems ) { 36 | assert.ok( true, 'no transition, removeComplete event did fire' ); 37 | assert.equal( h2Elems.length, removedItems.length, 'no transition, remove elems length matches argument length' ); 38 | assert.equal( container.children.length, expectedRemovedCount, 'no transition, elements removed from DOM' ); 39 | assert.equal( container.querySelectorAll('.h2').length, 0, 'no transition, matched elements were removed' ); 40 | setTimeout( removeNone, 20 ); 41 | // start(); 42 | }); 43 | 44 | olayer.remove( h2Elems ); 45 | } 46 | 47 | function removeNone() { 48 | var noneItems = container.querySelector('.foo'); 49 | olayer.remove( noneItems ); 50 | assert.ok( true, 'removing no items is cool' ); 51 | done(); 52 | } 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/stamp.js: -------------------------------------------------------------------------------- 1 | 2 | QUnit.test( 'stamp selector string', function( assert ) { 3 | var container = document.querySelector('#stamps1'); 4 | var stampElems = container.querySelectorAll('.stamp'); 5 | var stamp1 = container.querySelector('.stamp1'); 6 | var stamp2 = container.querySelector('.stamp2'); 7 | 8 | var layout = new Outlayer( container, { 9 | stamp: '.stamp' 10 | }); 11 | 12 | assert.equal( layout.stamps.length, stampElems.length, 'lenght matches' ); 13 | assert.equal( layout.stamps[0], stamp1, 'stamp1 matches' ); 14 | assert.equal( layout.stamps[1], stamp2, 'stamp2 matches' ); 15 | assert.ok( !stamp1.style.left, 'stamp 1 has no left style' ); 16 | assert.ok( !stamp1.style.top, 'stamp 1 has no top style' ); 17 | 18 | layout.destroy(); 19 | }); 20 | 21 | QUnit.test( 'stamp with NodeList', function( assert ) { 22 | var container = document.querySelector('#stamps1'); 23 | var stampElems = container.querySelectorAll('.stamp'); 24 | var stamp1 = container.querySelector('.stamp1'); 25 | var stamp2 = container.querySelector('.stamp2'); 26 | 27 | var layout = new Outlayer( container, { 28 | stamp: stampElems 29 | }); 30 | 31 | assert.equal( layout.stamps.length, stampElems.length, 'lenght matches' ); 32 | assert.equal( layout.stamps[0], stamp1, 'stamp1 matches' ); 33 | assert.equal( layout.stamps[1], stamp2, 'stamp2 matches' ); 34 | 35 | layout.destroy(); 36 | }); 37 | 38 | QUnit.test( 'stamp with array', function( assert ) { 39 | var container = document.querySelector('#stamps1'); 40 | var stampElems = container.querySelectorAll('.stamp'); 41 | var stamp1 = container.querySelector('.stamp1'); 42 | var stamp2 = container.querySelector('.stamp2'); 43 | 44 | var layout = new Outlayer( container, { 45 | stamp: [ stamp1, stamp2 ] 46 | }); 47 | 48 | assert.equal( layout.stamps.length, stampElems.length, 'lenght matches' ); 49 | assert.equal( layout.stamps[0], stamp1, 'stamp1 matches' ); 50 | assert.equal( layout.stamps[1], stamp2, 'stamp2 matches' ); 51 | 52 | layout.destroy(); 53 | }); 54 | 55 | QUnit.test( 'stamp and unstamp method', function( assert ) { 56 | var container = document.querySelector('#stamps1'); 57 | var stamp1 = container.querySelector('.stamp1'); 58 | var stamp2 = container.querySelector('.stamp2'); 59 | 60 | var layout = new Outlayer( container ); 61 | 62 | assert.equal( layout.stamps.length, 0, 'start with 0 stamps' ); 63 | 64 | layout.stamp( stamp1 ); 65 | assert.equal( layout.stamps.length, 1, 'stamp length = 1' ); 66 | assert.equal( layout.stamps[0], stamp1, 'stamp1 matches' ); 67 | 68 | layout.stamp('.stamp2'); 69 | assert.equal( layout.stamps.length, 2, 'stamp length = 2' ); 70 | assert.equal( layout.stamps[0], stamp1, 'stamp1 matches' ); 71 | assert.equal( layout.stamps[1], stamp2, 'stamp2 matches' ); 72 | 73 | layout.unstamp('.stamp1'); 74 | assert.equal( layout.stamps.length, 1, 'unstamped, and stamp length = 1' ); 75 | assert.equal( layout.stamps[0], stamp2, 'stamp2 matches' ); 76 | }); 77 | -------------------------------------------------------------------------------- /test/unit/transition-duration.js: -------------------------------------------------------------------------------- 1 | QUnit.test( 'transition duration', function( assert ) { 2 | 3 | var layout = new CellsByRow( '#transition-duration', { 4 | transitionDuration: '0s' 5 | }); 6 | 7 | var done = assert.async(); 8 | 9 | layout.options.columnWidth = 75; 10 | layout.options.rowHeight = 120; 11 | layout.once( 'layoutComplete', function() { 12 | assert.ok( true, 'layoutComplete triggered when transition duration = 0' ); 13 | done(); 14 | }); 15 | 16 | layout.layout(); 17 | 18 | }); 19 | --------------------------------------------------------------------------------