├── .gitignore
├── bower.json
├── package.json
├── sandbox
├── html-init.html
├── images.html
├── basic.html
├── adding-items.html
└── load-more-images.html
├── README.md
└── colcade.js
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "colcade",
3 | "description": "Lightweight masonry layout",
4 | "main": "colcade.js",
5 | "authors": [
6 | "David DeSandro"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "masonry"
11 | ],
12 | "homepage": "https://github.com/desandro/colcade",
13 | "ignore": [
14 | "**/.*",
15 | "node_modules",
16 | "bower_components",
17 | "test",
18 | "tests"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "colcade",
3 | "version": "0.2.0",
4 | "description": "Lightweight masonry layout",
5 | "main": "colcade.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/desandro/colcade.git"
12 | },
13 | "keywords": [
14 | "masonry"
15 | ],
16 | "author": "David DeSandro",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/desandro/colcade/issues"
20 | },
21 | "homepage": "https://github.com/desandro/colcade#readme"
22 | }
23 |
--------------------------------------------------------------------------------
/sandbox/html-init.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | html init
8 |
9 |
54 |
55 |
56 |
57 |
58 | html init
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/sandbox/images.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | images
8 |
9 |
55 |
56 |
57 |
58 |
59 | images
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/sandbox/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | basic
8 |
9 |
53 |
54 |
55 |
56 |
57 | basic
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/sandbox/adding-items.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | adding items
8 |
9 |
55 |
56 |
57 |
58 |
59 | adding items
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/sandbox/load-more-images.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | load more images
8 |
9 |
55 |
56 |
57 |
58 |
59 | load more images
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Colcade
2 |
3 | _Lightweight masonry layout_
4 |
5 | ## Colcade vs. Masonry
6 |
7 | [Masonry is great](http://masonry.desandro.com), but it has grown big as it has grown older. Colcade is designed to be small & fast. I recommend using Colcade over Masonry, but read over this feature comparison.
8 |
9 | ### Same features
10 |
11 | + Masonry grid layout
12 | + Works as a jQuery plugin or with vanilla JS
13 | + Initialize in HTML
14 |
15 | ### Better features
16 |
17 | + Much smaller. 1/8 the size of Masonry
18 | + Better fluid/responsive layout, using native browser positioning
19 | + One file, no dependencies, no package `dist` built file
20 | + Does not require [imagesLoaded when using images](http://masonry.desandro.com/layout.html#imagesloaded)
21 |
22 | ### Worse features
23 |
24 | + No multi-column-spanning items
25 |
26 | ```
27 | OK | No
28 | #### #### #### | ########## ####
29 | #### #### #### | ########## ####
30 | #### |
31 | #### #### | #### ##########
32 | #### #### #### | #### ##########
33 | #### #### #### | ####
34 | #### |
35 | ```
36 |
37 | + No built-in item transitions for [layout](http://masonry.desandro.com/methods.html#layout-masonry), [appending](http://masonry.desandro.com/methods.html#appended), [prepending](http://masonry.desandro.com/methods.html#prepended), or [removing](http://masonry.desandro.com/methods.html#remove)
38 | + No [stamps](http://masonry.desandro.com/options.html#stamp)
39 | + No [fitWidth centering](http://masonry.desandro.com/options.html#fitwidth)
40 |
41 | ## Install
42 |
43 | Download: [colcade.js](https://unpkg.com/colcade@0/colcade.js)
44 |
45 | CDN:
46 |
47 | ``` html
48 |
49 | ```
50 |
51 | [npm](https://www.npmjs.com/package/colcade): `npm install colcade`
52 |
53 | Bower: `bower install colcade`
54 |
55 | ## Demos
56 |
57 | + [Basic, HTML init](http://codepen.io/desandro/pen/EKMdxg)
58 | + [jQuery](http://codepen.io/desandro/pen/vGPVOR)
59 | + [Vanilla JS](http://codepen.io/desandro/pen/EKMdjN)
60 | + [Images](http://codepen.io/desandro/pen/pyYxvz)
61 |
62 | ## Usage
63 |
64 | Colcade works by moving item elements into column elements.
65 |
66 | ### HTML
67 |
68 | ``` html
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
...
77 |
...
78 |
...
79 | ...
80 |
81 | ```
82 |
83 | ### CSS
84 |
85 | Sizing of the columns is handled by your own CSS. Change the number of columns by hiding or showing them.
86 |
87 | ``` css
88 | /* Using floats */
89 | .grid-col {
90 | float: left;
91 | width: 50%;
92 | }
93 |
94 | /* 2 columns by default, hide columns 2 & 3 */
95 | .grid-col--2, .grid-col--3 { display: none }
96 |
97 | /* 3 columns at medium size */
98 | @media ( min-width: 768px ) {
99 | .grid-col { width: 33.333%; }
100 | .grid-col--2 { display: block; } /* show column 2 */
101 | }
102 |
103 | /* 4 columns at large size */
104 | @media ( min-width: 1080px ) {
105 | .grid-col { width: 25%; }
106 | .grid-col--3 { display: block; } /* show column 3 */
107 | }
108 | ```
109 |
110 | [Edit float demo on CodePen](http://codepen.io/desandro/pen/EKMdxg)
111 |
112 | ``` css
113 | /* with flexbox */
114 | .grid {
115 | display: -webkit-box;
116 | display: -webkit-flex;
117 | display: -ms-flexbox;
118 | display: flex;
119 | }
120 |
121 | .grid-col {
122 | -webkit-box-flex: 1;
123 | -webkit-flex-grow: 1;
124 | -ms-flex-positive: 1;
125 | flex-grow: 1;
126 | }
127 |
128 | /* 2 columns by default, hide columns 2 & 3 */
129 | .grid-col--2, .grid-col--3 { display: none }
130 |
131 | /* 3 columns at medium size */
132 | @media ( min-width: 768px ) {
133 | .grid-col--2 { display: block; } /* show column 2 */
134 | }
135 |
136 | /* 4 columns at large size */
137 | @media ( min-width: 1080px ) {
138 | .grid-col--3 { display: block; } /* show column 3 */
139 | }
140 | ```
141 |
142 | [Edit flexbox demo on CodePen](http://codepen.io/desandro/pen/RadeWy)
143 |
144 | ### Initialize Colcade
145 |
146 | Set selectors for column and item elements in the options.
147 |
148 | With jQuery. [Edit jQuery demo on CodePen](http://codepen.io/desandro/pen/vGPVOR)
149 |
150 | ``` js
151 | $('.grid').colcade({
152 | columns: '.grid-col',
153 | items: '.grid-item'
154 | })
155 | ```
156 |
157 | With vanilla JS. [Edit vanilla JS demo on CodePen](http://codepen.io/desandro/pen/EKMdjN)
158 |
159 | ``` js
160 | // element as first argument
161 | var grid = document.querySelector('.grid');
162 | var colc = new Colcade( grid, {
163 | columns: '.grid-col',
164 | items: '.grid-item'
165 | });
166 |
167 | // selector string as first argument
168 | var colc = new Colcade( '.grid', {
169 | columns: '.grid-col',
170 | items: '.grid-item'
171 | });
172 | ```
173 |
174 | With HTML. [Edit HTML demo on CodePen](http://codepen.io/desandro/pen/EKMdxg)
175 |
176 | ``` html
177 |
178 | ...
179 |
180 | ```
181 |
182 | ## Methods
183 |
184 | ### append
185 |
186 | Add items to end of layout.
187 |
188 | ``` js
189 | // jQuery
190 | $grid.colcade( 'append', items )
191 | // vanilla JS
192 | colc.append( items )
193 | ```
194 |
195 | ### prepend
196 |
197 | Add items to beginning of layout.
198 |
199 | ``` js
200 | // jQuery
201 | $grid.colcade( 'prepend', items )
202 | // vanilla JS
203 | colc.prepend( items )
204 | ```
205 |
206 | ### destroy
207 |
208 | Remove Colcade behavior completely.
209 |
210 | ``` js
211 | // jQuery
212 | $grid.colcade('destroy')
213 | // vanilla JS
214 | colc.destroy()
215 | ```
216 |
217 | ---
218 |
219 | By David DeSandro
220 |
221 | MIT License. Have at it.
222 |
--------------------------------------------------------------------------------
/colcade.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Colcade v0.2.0
3 | * Lightweight masonry layout
4 | * by David DeSandro
5 | * MIT license
6 | */
7 |
8 | /*jshint browser: true, undef: true, unused: true */
9 |
10 | ( function( window, factory ) {
11 | // universal module definition
12 | /*jshint strict: false */
13 | /*global define: false, module: false */
14 | if ( typeof define == 'function' && define.amd ) {
15 | // AMD
16 | define( factory );
17 | } else if ( typeof module == 'object' && module.exports ) {
18 | // CommonJS
19 | module.exports = factory();
20 | } else {
21 | // browser global
22 | window.Colcade = factory();
23 | }
24 |
25 | }( window, function factory() {
26 |
27 | // -------------------------- Colcade -------------------------- //
28 |
29 | function Colcade( element, options ) {
30 | element = getQueryElement( element );
31 |
32 | // do not initialize twice on same element
33 | if ( element && element.colcadeGUID ) {
34 | var instance = instances[ element.colcadeGUID ];
35 | instance.option( options );
36 | return instance;
37 | }
38 |
39 | this.element = element;
40 | // options
41 | this.options = {};
42 | this.option( options );
43 | // kick things off
44 | this.create();
45 | }
46 |
47 | var proto = Colcade.prototype;
48 |
49 | proto.option = function( options ) {
50 | this.options = extend( this.options, options );
51 | };
52 |
53 | // globally unique identifiers
54 | var GUID = 0;
55 | // internal store of all Colcade intances
56 | var instances = {};
57 |
58 | proto.create = function() {
59 | this.errorCheck();
60 | // add guid for Colcade.data
61 | var guid = this.guid = ++GUID;
62 | this.element.colcadeGUID = guid;
63 | instances[ guid ] = this; // associate via id
64 | // update initial properties & layout
65 | this.reload();
66 | // events
67 | this._windowResizeHandler = this.onWindowResize.bind( this );
68 | this._loadHandler = this.onLoad.bind( this );
69 | window.addEventListener( 'resize', this._windowResizeHandler );
70 | this.element.addEventListener( 'load', this._loadHandler, true );
71 | };
72 |
73 | proto.errorCheck = function() {
74 | var errors = [];
75 | if ( !this.element ) {
76 | errors.push( 'Bad element: ' + this.element );
77 | }
78 | if ( !this.options.columns ) {
79 | errors.push( 'columns option required: ' + this.options.columns );
80 | }
81 | if ( !this.options.items ) {
82 | errors.push( 'items option required: ' + this.options.items );
83 | }
84 |
85 | if ( errors.length ) {
86 | throw new Error( '[Colcade error] ' + errors.join('. ') );
87 | }
88 | };
89 |
90 | // update properties and do layout
91 | proto.reload = function() {
92 | this.updateColumns();
93 | this.updateItems();
94 | this.layout();
95 | };
96 |
97 | proto.updateColumns = function() {
98 | this.columns = querySelect( this.options.columns, this.element );
99 | };
100 |
101 | proto.updateItems = function() {
102 | this.items = querySelect( this.options.items, this.element );
103 | };
104 |
105 | proto.getActiveColumns = function() {
106 | return this.columns.filter( function( column ) {
107 | var style = getComputedStyle( column );
108 | return style.display != 'none';
109 | });
110 | };
111 |
112 | // ----- layout ----- //
113 |
114 | // public, updates activeColumns
115 | proto.layout = function() {
116 | this.activeColumns = this.getActiveColumns();
117 | this._layout();
118 | };
119 |
120 | // private, does not update activeColumns
121 | proto._layout = function() {
122 | // reset column heights
123 | this.columnHeights = this.activeColumns.map( function() {
124 | return 0;
125 | });
126 | // layout all items
127 | this.layoutItems( this.items );
128 | };
129 |
130 | proto.layoutItems = function( items ) {
131 | items.forEach( this.layoutItem, this );
132 | };
133 |
134 | proto.layoutItem = function( item ) {
135 | // layout item by appending to column
136 | var minHeight = Math.min.apply( Math, this.columnHeights );
137 | var index = this.columnHeights.indexOf( minHeight );
138 | this.activeColumns[ index ].appendChild( item );
139 | // at least 1px, if item hasn't loaded
140 | // Not exactly accurate, but it's cool
141 | this.columnHeights[ index ] += item.offsetHeight || 1;
142 | };
143 |
144 | // ----- adding items ----- //
145 |
146 | proto.append = function( elems ) {
147 | var items = this.getQueryItems( elems );
148 | // add items to collection
149 | this.items = this.items.concat( items );
150 | // lay them out
151 | this.layoutItems( items );
152 | };
153 |
154 | proto.prepend = function( elems ) {
155 | var items = this.getQueryItems( elems );
156 | // add items to collection
157 | this.items = items.concat( this.items );
158 | // lay out everything
159 | this._layout();
160 | };
161 |
162 | proto.getQueryItems = function( elems ) {
163 | elems = makeArray( elems );
164 | var fragment = document.createDocumentFragment();
165 | elems.forEach( function( elem ) {
166 | fragment.appendChild( elem );
167 | });
168 | return querySelect( this.options.items, fragment );
169 | };
170 |
171 | // ----- measure column height ----- //
172 |
173 | proto.measureColumnHeight = function( elem ) {
174 | var boundingRect = this.element.getBoundingClientRect();
175 | this.activeColumns.forEach( function( column, i ) {
176 | // if elem, measure only that column
177 | // if no elem, measure all columns
178 | if ( !elem || column.contains( elem ) ) {
179 | var lastChildRect = column.lastElementChild.getBoundingClientRect();
180 | // not an exact calculation as it includes top border, and excludes item bottom margin
181 | this.columnHeights[ i ] = lastChildRect.bottom - boundingRect.top;
182 | }
183 | }, this );
184 | };
185 |
186 | // ----- events ----- //
187 |
188 | proto.onWindowResize = function() {
189 | clearTimeout( this.resizeTimeout );
190 | this.resizeTimeout = setTimeout( function() {
191 | this.onDebouncedResize();
192 | }.bind( this ), 100 );
193 | };
194 |
195 | proto.onDebouncedResize = function() {
196 | var activeColumns = this.getActiveColumns();
197 | // check if columns changed
198 | var isSameLength = activeColumns.length == this.activeColumns.length;
199 | var isSameColumns = true;
200 | this.activeColumns.forEach( function( column, i ) {
201 | isSameColumns = isSameColumns && column == activeColumns[i];
202 | });
203 | if ( isSameLength && isSameColumns ) {
204 | return;
205 | }
206 | // activeColumns changed
207 | this.activeColumns = activeColumns;
208 | this._layout();
209 | };
210 |
211 | proto.onLoad = function( event ) {
212 | this.measureColumnHeight( event.target );
213 | };
214 |
215 | // ----- destroy ----- //
216 |
217 | proto.destroy = function() {
218 | // move items back to container
219 | this.items.forEach( function( item ) {
220 | this.element.appendChild( item );
221 | }, this );
222 | // remove events
223 | window.removeEventListener( 'resize', this._windowResizeHandler );
224 | this.element.removeEventListener( 'load', this._loadHandler, true );
225 | // remove data
226 | delete this.element.colcadeGUID;
227 | delete instances[ this.guid ];
228 | };
229 |
230 | // -------------------------- HTML init -------------------------- //
231 |
232 | docReady( function() {
233 | var dataElems = querySelect('[data-colcade]');
234 | dataElems.forEach( htmlInit );
235 | });
236 |
237 | function htmlInit( elem ) {
238 | // convert attribute "foo: bar, qux: baz" into object
239 | var attr = elem.getAttribute('data-colcade');
240 | var attrParts = attr.split(',');
241 | var options = {};
242 | attrParts.forEach( function( part ) {
243 | var pair = part.split(':');
244 | var key = pair[0].trim();
245 | var value = pair[1].trim();
246 | options[ key ] = value;
247 | });
248 |
249 | new Colcade( elem, options );
250 | }
251 |
252 | Colcade.data = function( elem ) {
253 | elem = getQueryElement( elem );
254 | var id = elem && elem.colcadeGUID;
255 | return id && instances[ id ];
256 | };
257 |
258 | // -------------------------- jQuery -------------------------- //
259 |
260 | Colcade.makeJQueryPlugin = function( $ ) {
261 | $ = $ || window.jQuery;
262 | if ( !$ ) {
263 | return;
264 | }
265 |
266 | $.fn.colcade = function( arg0 /*, arg1 */) {
267 | // method call $().colcade( 'method', { options } )
268 | if ( typeof arg0 == 'string' ) {
269 | // shift arguments by 1
270 | var args = Array.prototype.slice.call( arguments, 1 );
271 | return methodCall( this, arg0, args );
272 | }
273 | // just $().colcade({ options })
274 | plainCall( this, arg0 );
275 | return this;
276 | };
277 |
278 | function methodCall( $elems, methodName, args ) {
279 | var returnValue;
280 | $elems.each( function( i, elem ) {
281 | // get instance
282 | var colcade = $.data( elem, 'colcade' );
283 | if ( !colcade ) {
284 | return;
285 | }
286 | // apply method, get return value
287 | var value = colcade[ methodName ].apply( colcade, args );
288 | // set return value if value is returned, use only first value
289 | returnValue = returnValue === undefined ? value : returnValue;
290 | });
291 | return returnValue !== undefined ? returnValue : $elems;
292 | }
293 |
294 | function plainCall( $elems, options ) {
295 | $elems.each( function( i, elem ) {
296 | var colcade = $.data( elem, 'colcade' );
297 | if ( colcade ) {
298 | // set options & init
299 | colcade.option( options );
300 | colcade.layout();
301 | } else {
302 | // initialize new instance
303 | colcade = new Colcade( elem, options );
304 | $.data( elem, 'colcade', colcade );
305 | }
306 | });
307 | }
308 | };
309 |
310 | // try making plugin
311 | Colcade.makeJQueryPlugin();
312 |
313 | // -------------------------- utils -------------------------- //
314 |
315 | function extend( a, b ) {
316 | for ( var prop in b ) {
317 | a[ prop ] = b[ prop ];
318 | }
319 | return a;
320 | }
321 |
322 | // turn element or nodeList into an array
323 | function makeArray( obj ) {
324 | var ary = [];
325 | if ( Array.isArray( obj ) ) {
326 | // use object if already an array
327 | ary = obj;
328 | } else if ( obj && typeof obj.length == 'number' ) {
329 | // convert nodeList to array
330 | for ( var i=0; i < obj.length; i++ ) {
331 | ary.push( obj[i] );
332 | }
333 | } else {
334 | // array of single index
335 | ary.push( obj );
336 | }
337 | return ary;
338 | }
339 |
340 | // get array of elements
341 | function querySelect( selector, elem ) {
342 | elem = elem || document;
343 | var elems = elem.querySelectorAll( selector );
344 | return makeArray( elems );
345 | }
346 |
347 | function getQueryElement( elem ) {
348 | if ( typeof elem == 'string' ) {
349 | elem = document.querySelector( elem );
350 | }
351 | return elem;
352 | }
353 |
354 | function docReady( onReady ) {
355 | if ( document.readyState == 'complete' ) {
356 | onReady();
357 | return;
358 | }
359 | document.addEventListener( 'DOMContentLoaded', onReady );
360 | }
361 |
362 | // -------------------------- end -------------------------- //
363 |
364 | return Colcade;
365 |
366 | }));
367 |
--------------------------------------------------------------------------------