diff --git a/Gruntfile.js b/Gruntfile.js index 33980c3..34ab096 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,7 +2,7 @@ // our wrapper function (required by grunt and its plugins) // all configuration goes inside this function -module.exports = function (grunt) { +module.exports = function(grunt) { // =========================================================================== // CONFIGURE GRUNT =========================================================== @@ -17,16 +17,26 @@ module.exports = function (grunt) { separator: ';' }, dist: { - src: ['src/module.js', - 'src/dynamic-layout.directive.js', - 'src/layout-on-load.directive.js', - 'src/filter.service.js', - 'src/position.service.js', - 'src/ranker.service.js', - 'src/as.filter.js', - 'src/custom-filter.filter.js', - 'src/custom-ranker.filter.js'], - dest: 'dist/<%= pkg.name %>.js' + src: ['src/js/module.js', + 'src/js/dynamic-layout.directive.js', + 'src/js/layout-on-load.directive.js', + 'src/js/filter.service.js', + 'src/js/position.service.js', + 'src/js/ranker.service.js', + 'src/js/as.filter.js', + 'src/js/custom-filter.filter.js', + 'src/js/custom-ranker.filter.js'], + dest: 'dist/js/<%= pkg.name %>.js' + } + }, + ngAnnotate: { + options: { + singleQuotes: true, + }, + dist: { + files: { + 'dist/js/<%= pkg.name %>.js': 'dist/js/<%= pkg.name %>.js' + } } }, karma: { @@ -35,12 +45,12 @@ module.exports = function (grunt) { singleRun: true } }, - jshint: { + eslint: { // when this task is run, lint the Gruntfile and all js files in src options: { - multistr: true, + configFile: '.eslintrc', }, - build: ['Grunfile.js', 'src/**/*.js', 'tests/**/*.js'] + build: ['Gruntfile.js', 'src/**/*.js', 'tests/**/*.js'] }, uglify: { options: { @@ -49,7 +59,7 @@ module.exports = function (grunt) { }, build: { files: { - 'dist/<%= pkg.name %>.min.js': ['dist/<%= pkg.name %>.js'], + 'dist/js/<%= pkg.name %>.min.js': ['dist/js/<%= pkg.name %>.js'], } } } @@ -61,8 +71,9 @@ module.exports = function (grunt) { // we can only load these if they are in our package.json // make sure you have run npm install so our app can find these grunt.loadNpmTasks('grunt-karma'); - grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('gruntify-eslint'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.registerTask('default', ['karma', 'jshint', 'concat', 'uglify']); + grunt.loadNpmTasks('grunt-ng-annotate'); + grunt.registerTask('default', ['eslint', 'karma', 'concat', 'ngAnnotate', 'uglify']); }; diff --git a/bower.json b/bower.json index 4b21f46..c270d85 100644 --- a/bower.json +++ b/bower.json @@ -3,11 +3,13 @@ "version": "0.1.6", "main": "dist/angular-dynamic-layout.min.js", "dependencies": { - "angular": "1.3.5", - "angular-animate": "1.3.5" + "angular": "~1.4.0", + "angular-animate": "~1.4.0" }, "devDependencies": { - "angular-mocks": "1.3.5" + "angular-mocks": "~1.4.0", + "bootcards": "latest", + "bootstrap": "latest" }, "homepage": "https://github.com/tristanguigue/angular-dynamic-layout", "authors": [ diff --git a/dist/angular-dynamic-layout.js b/dist/angular-dynamic-layout.js deleted file mode 100644 index 1739105..0000000 --- a/dist/angular-dynamic-layout.js +++ /dev/null @@ -1,825 +0,0 @@ -(function() { - 'use strict'; - - angular - .module('dynamicLayout', [ 'ngAnimate' ]); - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .directive('dynamicLayout', ['$timeout', '$window', '$q', '$animate', 'PositionService', dynamicLayout]); - - /* - * The isotope directive that renders the templates based on the array of items - * passed - * @scope items: the list of items to be rendered - * @scope rankers: the rankers to be applied on the list of items - * @scope filters: the filters to be applied on the list of items - * @scope defaulttemplate: (optional) the deafult template to be applied on each item if no item template is defined - */ - function dynamicLayout($timeout, $window, $q, $animate, PositionService) { - - return { - restrict: 'A', - scope: { - items: '=', - rankers: '=', - filters: '=', - defaulttemplate: '=?' - }, - template: '
', - link: link - }; - - function link(scope, element) { - - // Keep count of the number of templates left to load - scope.templatesToLoad = 0; - scope.externalScope = externalScope; - - // Fires when a template is requested through the ng-include directive - scope.$on('$includeContentRequested', function() { - scope.templatesToLoad++; - }); - - // Fires when a template has been loaded through the ng-include - // directive - scope.$on('$includeContentLoaded', function() { - scope.templatesToLoad--; - }); - - /* - * Triggers a layout every time the items are changed - */ - scope.$watch('filteredItems', function(newValue, oldValue) { - // We want the filteredItems to be available to the controller - // This feels hacky, there must be a better way to do this - scope.$parent.filteredItems = scope.filteredItems; - - if (!angular.equals(newValue, oldValue)) { - itemsLoaded().then(function() { - layout(); - }); - } - }, true); - - /* - * Triggers a layout every time the window is resized - */ - angular.element($window).bind('resize', function() { - // We need to apply the scope - scope.$apply(function() { - layout(); - }); - }); - - /* - * Triggers a layout whenever requested by an external source - * Allows a callback to be fired after the layout animation is - * completed - */ - scope.$on('layout', function(event, callback) { - layout().then(function() { - if (angular.isFunction('function')) { - callback(); - } - }); - }); - - /* - * Triggers the initial layout once all the templates are loaded - */ - itemsLoaded().then(function() { - layout(); - }); - - /* - * Use the PositionService to layout the items - * @return the promise of the cards being animated - */ - function layout() { - return PositionService.layout(element[0].offsetWidth); - } - - /* - * Check when all the items have been loaded by the ng-include - * directive - */ - function itemsLoaded() { - var def = $q.defer(); - - // $timeout : We need to wait for the includeContentRequested to - // be called before we can assume there is no templates to be loaded - $timeout(function() { - if (scope.templatesToLoad === 0) { - def.resolve(); - } - }); - - scope.$watch('templatesToLoad', function(newValue, oldValue) { - if (newValue !== oldValue && scope.templatesToLoad === 0) { - def.resolve(); - } - }); - - return def.promise; - } - - /* - * This allows the external scope, that is the scope of - * dynamic-layout's container to be called from the templates - * @return the given scope - */ - function externalScope() { - return scope.$parent; - } - - } - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .directive('layoutOnLoad', ['$rootScope', layoutOnLoad]); - - /* - * Directive on images to layout after each load - */ - function layoutOnLoad($rootScope) { - - return { - restrict: 'A', - link: function(scope, element) { - element.bind('load error', function() { - $rootScope.$broadcast('layout'); - }); - } - }; - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .factory('FilterService', FilterService); - - /* - * The filter service - * - * COMPARATORS = ['=', '<', '>', '<=', '>=', '!=', 'in', 'not in', 'contains'] - * - * Allows filters in Conjuctive Normal Form using the item's property or any - * custom operation on the items - * - * For example: - * var filters = [ // an AND goup compose of OR groups - * [ // an OR group compose of statements - * ['color', '=', 'grey'], // A statement - * ['color', '=', 'black'] - * ], - * [ // a second OR goup composed of statements - * ['atomicNumber', '<', 3] - * ] - * ]; - * Or - * var myCustomFilter = function(item){ - * if(item.color != 'red') - * return true; - * else - * return false; - * }; - * - * filters = [ - * [myCustomFilter] - * ]; - * - */ - function FilterService() { - - return { - applyFilters: applyFilters - }; - - /* - * Check which of the items passes the filters - * @param items: the items being probed - * @param filters: the array of and groups use to probe the item - * @return the list of items that passes the filters - */ - function applyFilters(items, filters) { - var retItems = []; - var i; - for (i in items) { - if (checkAndGroup(items[i], filters)) { - retItems.push(items[i]); - } - } - return retItems; - } - - /* - * Check if a single item passes the single statement criteria - * @param item: the item being probed - * @param statement: the criteria being use to test the item - * @return true if the item passed the statement, false otherwise - */ - function checkStatement(item, statement) { - // If the statement is a custom filter, we give the item as a parameter - if (angular.isFunction(statement)) { - return statement(item); - } - - // If the statement is a regular filter, it has to be with the form - // [propertyName, comparator, value] - - var STATEMENT_LENGTH = 3; - if (statement.length < STATEMENT_LENGTH) { - throw 'Incorrect statement'; - } - - var property = statement[0]; - var comparator = statement[1]; - var value = statement[2]; - - // If the property is not found in the item then we consider the - // statement to be false - if (!item[property]) { - return false; - } - - switch (comparator) { - case '=': - return item[property] === value; - case '<': - return item[property] < value; - case '<=': - return item[property] <= value; - case '>': - return item[property] > value; - case '>=': - return item[property] >= value; - case '!=': - return item[property] !== value; - case 'in': - return item[property] in value; - case 'not in': - return !(item[property] in value); - case 'contains': - if (!(item[property] instanceof Array)) { - throw 'contains statement has to be applied on array'; - } - return item[property].indexOf(value) > -1; - default: - throw 'Incorrect statement comparator: ' + comparator; - } - } - - /* - * Check a sub (or) group - * @param item: the item being probed - * @param orGroup: the array of statement use to probe the item - * @return true if the item passed at least one of the statements, - * false otherwise - */ - function checkOrGroup(item, orGroup) { - var j; - for (j in orGroup) { - if (checkStatement(item, orGroup[j])) { - return true; - } - } - return false; - } - - /* - * Check the main group - * @param item: the item being probed - * @param orGroup: the array of or groups use to probe the item - * @return true if the item passed all of of the or groups, - * false otherwise - */ - function checkAndGroup(item, andGroup) { - var i; - for (i in andGroup) { - if (!checkOrGroup(item, andGroup[i])) { - return false; - } - } - return true; - } - - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .factory('PositionService', ['$window', '$document', '$animate', '$timeout', '$q', PositionService]); - - /* - * The position service - * - * Find the best adjustements of the elemnts in the DOM according the their - * order, height and width - * - * Fix their absolute position in the DOM while adding a ng-animate class for - * personalized animations - * - */ - function PositionService($window, $document, $animate, $timeout, $q) { - - // The list of ongoing animations - var ongoingAnimations = {}; - // The list of items related to the DOM elements - var items = []; - // The list of the DOM elements - var elements = []; - // The columns that contains the items - var columns = []; - - var self = { - getItemsDimensionFromDOM: getItemsDimensionFromDOM, - applyToDOM: applyToDOM, - layout: layout, - getColumns: getColumns - }; - return self; - - /* - * Get the items heights and width from the DOM - * @return: the list of items with their sizes - */ - function getItemsDimensionFromDOM() { - // not(.ng-leave) : we don't want to select elements that have been - // removed but are still in the DOM - elements = $document[0].querySelectorAll( - '.dynamic-layout-item-parent:not(.ng-leave)' - ); - items = []; - for (var i = 0; i < elements.length; ++i) { - // Note: we need to get the children element width because that's - // where the style is applied - var rect = elements[i].children[0].getBoundingClientRect(); - var width; - var height; - if (rect.width) { - width = rect.width; - height = rect.height; - } else { - width = rect.right - rect.left; - height = rect.top - rect.bottom; - } - - items.push({ - height: height + - parseFloat($window.getComputedStyle(elements[i]).marginTop), - width: width + - parseFloat( - $window.getComputedStyle(elements[i].children[0]).marginLeft - ) - }); - } - return items; - } - - /* - * Apply positions to the DOM with an animation - * @return: the promise of the position animations being completed - */ - function applyToDOM() { - - var ret = $q.defer(); - - /* - * Launch an animation on a specific element - * Once the animation is complete remove it from the ongoing animation - * @param element: the element being moved - * @param i: the index of the current animation - * @return: the promise of the animation being completed - */ - function launchAnimation(element, i) { - var animationPromise = $animate.addClass(element, - 'move-items-animation', - { - from: { - position: 'absolute' - }, - to: { - left: items[i].x + 'px', - top: items[i].y + 'px' - } - } - ); - - animationPromise.then(function() { - // We remove the class so that the animation can be ran again - element.classList.remove('move-items-animation'); - delete ongoingAnimations[i]; - }); - - return animationPromise; - } - - /* - * Launch the animations on all the elements - * @return: the promise of the animations being completed - */ - function launchAnimations() { - var i; - for (i = 0; i < items.length; ++i) { - // We need to pass the specific element we're dealing with - // because at the next iteration elements[i] might point to - // something else - ongoingAnimations[i] = launchAnimation(elements[i], i); - } - $q.all(ongoingAnimations).then(function() { - ret.resolve(); - }); - } - - // We need to cancel all ongoing animations before we start the new - // ones - if (Object.keys(ongoingAnimations).length) { - for (var j in ongoingAnimations) { - $animate.cancel(ongoingAnimations[j]); - delete ongoingAnimations[j]; - } - } - - // For some reason we need to launch the new animations at the next - // digest - $timeout(function() { - launchAnimations(ret); - }); - - return ret.promise; - } - - /* - * Apply the position service on the elements in the DOM - * @param containerWidth: the width of the dynamic-layout container - * @return: the promise of the position animations being completed - */ - function layout(containerWidth) { - // We first gather the items dimension based on the DOM elements - items = self.getItemsDimensionFromDOM(); - - // Then we get the column size base the elements minimum width - var colSize = getColSize(); - var nbColumns = Math.floor(containerWidth / colSize); - // We create empty columns to be filled with the items - initColumns(nbColumns); - - // We determine what is the column size of each of the items based on - // their width and the column size - setItemsColumnSpan(colSize); - - // We set what should be their absolute position in the DOM - setItemsPosition(columns, colSize); - - // We apply those positions to the DOM with an animation - return self.applyToDOM(); - } - - // Make the columns public - function getColumns() { - return columns; - } - - /* - * Intialize the columns - * @param nb: the number of columns to be initialized - * @return: the empty columns - */ - function initColumns(nb) { - columns = []; - var i; - for (i = 0; i < nb; ++i) { - columns.push([]); - } - return columns; - } - - /* - * Get the columns heights - * @param columns: the columns with the items they contain - * @return: an array of columns heights - */ - function getColumnsHeights(cols) { - var columnsHeights = []; - var i; - for (i in cols) { - var h; - if (cols[i].length) { - var lastItem = cols[i][cols[i].length - 1]; - h = lastItem.y + lastItem.height; - } else { - h = 0; - } - columnsHeights.push(h); - } - return columnsHeights; - } - - /* - * Find the item absolute position and what columns it belongs too - * @param item: the item to place - * @param colHeights: the current heigh of the column when all items prior to this - * one were places - * @param colSize: the column size - * @return the item's columms and coordinates - */ - function getItemColumnsAndPosition(item, colHeights, colSize) { - if (item.columnSpan > colHeights.length) { - throw 'Item too large'; - } - - var indexOfMin = 0; - var minFound = 0; - var i; - - // We look at what set of columns have the minimum height - for (i = 0; i <= colHeights.length - item.columnSpan; ++i) { - var startingColumn = i; - var endingColumn = i + item.columnSpan; - var maxHeightInPart = Math.max.apply( - Math, colHeights.slice(startingColumn, endingColumn) - ); - - if (i === 0 || maxHeightInPart < minFound) { - minFound = maxHeightInPart; - indexOfMin = i; - } - } - - var itemColumns = []; - for (i = indexOfMin; i < indexOfMin + item.columnSpan; ++i) { - itemColumns.push(i); - } - - var position = { - x: itemColumns[0] * colSize, - y: minFound - }; - - return { - columns: itemColumns, - position: position - }; - } - - /* - * Set the items' absolute position - * @param columns: the empty columns - * @param colSize: the column size - */ - function setItemsPosition(cols, colSize) { - var i; - var j; - for (i = 0; i < items.length; ++i) { - var columnsHeights = getColumnsHeights(cols); - - var itemColumnsAndPosition = getItemColumnsAndPosition(items[i], - columnsHeights, - colSize); - - // We place the item in the found columns - for (j in itemColumnsAndPosition.columns) { - columns[itemColumnsAndPosition.columns[j]].push(items[i]); - } - - items[i].x = itemColumnsAndPosition.position.x; - items[i].y = itemColumnsAndPosition.position.y; - } - } - - /* - * Get the column size based on the minimum width of the items - * @return: column size - */ - function getColSize() { - var colSize; - var i; - for (i = 0; i < items.length; ++i) { - if (!colSize || items[i].width < colSize) { - colSize = items[i].width; - } - } - return colSize; - } - - /* - * Set the column span for each of the items based on their width and the - * column size - * @param: column size - */ - function setItemsColumnSpan(colSize) { - var i; - for (i = 0; i < items.length; ++i) { - items[i].columnSpan = Math.ceil(items[i].width / colSize); - } - } - - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .factory('RankerService', RankerService); - - /* - * The rankers service - * - * Allows a list of rankers to sort the items. - * If two items are the same regarding the first ranker, the second one is used - * to part them, etc. - * - * Rankers can be either a property name or a custom operation on the item. - * They all need to specify the order chosen (asc' or 'desc') - * - * var rankers = [ - * ['color', 'asc'], - * ['atomicNumber', 'desc'] - * ]; - * Or - * var rankers = [ - * [myCustomGetter, 'asc'] - * ]; - * - */ - function RankerService() { - - return { - applyRankers: applyRankers - }; - - /* - * Order the items with the given rankers - * @param items: the items being ranked - * @param rankers: the array of rankers used to rank the items - * @return the ordered list of items - */ - function applyRankers(items, rankers) { - // The ranker counter - var i = 0; - - if (rankers) { - items.sort(sorter); - } - - /* - * The custom sorting function using the built comparison function - * @param a, b: the items to be compared - * @return -1, 0 or 1 - */ - function sorter(a, b) { - i = 0; - return recursiveRanker(a, b); - } - - /* - * Compare recursively two items - * It first compare the items with the first ranker, if no conclusion - * can be drawn it uses the second ranker and so on until it finds a - * winner or there are no more rankers - * @param a, b: the items to be compared - * @return -1, 0 or 1 - */ - function recursiveRanker(a, b) { - var ranker = rankers[i][0]; - var ascDesc = rankers[i][1]; - var valueA; - var valueB; - // If it is a custom ranker, give the item as input and gather the - // ouput - if (angular.isFunction(ranker)) { - valueA = ranker(a); - valueB = ranker(b); - } else { - // Otherwise use the item's properties - if (!(ranker in a) && !(ranker in b)) { - valueA = 0; - valueB = 0; - } else if (!(ranker in a)) { - return ascDesc === 'asc' ? -1 : 1; - } else if (!(ranker in b)) { - return ascDesc === 'asc' ? 1 : -1; - } - valueA = a[ranker]; - valueB = b[ranker]; - } - - if (typeof valueA === typeof valueB) { - - if (angular.isString(valueA)) { - var comp = valueA.localeCompare(valueB); - if (comp === 1) { - return ascDesc === 'asc' ? 1 : -1; - } else if (comp === -1) { - return ascDesc === 'asc' ? -1 : 1; - } - } else { - if (valueA > valueB) { - return ascDesc === 'asc' ? 1 : -1; - } else if (valueA < valueB) { - return ascDesc === 'asc' ? -1 : 1; - } - } - } - - ++i; - - if (rankers.length > i) { - return recursiveRanker(a, b); - } - - return 0; - } - - return items; - } - - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .filter('as', ['$parse', as]); - - /* - * This allowed the result of the filters to be assigned to the scope - */ - function as($parse) { - - return function(value, context, path) { - $parse(path).assign(context, value); - return value; - }; - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .filter('customFilter', ['FilterService', customFilter]); - - /* - * The filter to be applied on the ng-repeat directive - */ - function customFilter(FilterService) { - - return function(items, filters) { - if (filters) { - return FilterService.applyFilters(items, filters); - } - return items; - }; - } - -})(); -;(function() { - 'use strict'; - - angular - .module('dynamicLayout') - .filter('customRanker', ['RankerService', customRanker]); - - /* - * The ranker to be applied on the ng-repeat directive - */ - function customRanker(RankerService) { - - return function(items, rankers) { - if (rankers) { - return RankerService.applyRankers(items, rankers); - } - return items; - }; - } - -})(); diff --git a/dist/angular-dynamic-layout.min.js b/dist/angular-dynamic-layout.min.js deleted file mode 100644 index bcef0de..0000000 --- a/dist/angular-dynamic-layout.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/* - angular-dynamic-layout 2015-09-13 -*/ -!function(){"use strict";angular.module("dynamicLayout",["ngAnimate"])}(),function(){"use strict";function dynamicLayout($timeout,$window,$q,$animate,PositionService){function link(scope,element){function layout(){return PositionService.layout(element[0].offsetWidth)}function itemsLoaded(){var def=$q.defer();return $timeout(function(){0===scope.templatesToLoad&&def.resolve()}),scope.$watch("templatesToLoad",function(newValue,oldValue){newValue!==oldValue&&0===scope.templatesToLoad&&def.resolve()}),def.promise}function externalScope(){return scope.$parent}scope.templatesToLoad=0,scope.externalScope=externalScope,scope.$on("$includeContentRequested",function(){scope.templatesToLoad++}),scope.$on("$includeContentLoaded",function(){scope.templatesToLoad--}),scope.$watch("filteredItems",function(newValue,oldValue){scope.$parent.filteredItems=scope.filteredItems,angular.equals(newValue,oldValue)||itemsLoaded().then(function(){layout()})},!0),angular.element($window).bind("resize",function(){scope.$apply(function(){layout()})}),scope.$on("layout",function(event,callback){layout().then(function(){angular.isFunction("function")&&callback()})}),itemsLoaded().then(function(){layout()})}return{restrict:"A",scope:{items:"=",rankers:"=",filters:"=",defaulttemplate:"=?"},template:'',link:link}}angular.module("dynamicLayout").directive("dynamicLayout",["$timeout","$window","$q","$animate","PositionService",dynamicLayout])}(),function(){"use strict";function layoutOnLoad($rootScope){return{restrict:"A",link:function(scope,element){element.bind("load error",function(){$rootScope.$broadcast("layout")})}}}angular.module("dynamicLayout").directive("layoutOnLoad",["$rootScope",layoutOnLoad])}(),function(){"use strict";function FilterService(){function applyFilters(items,filters){var i,retItems=[];for(i in items)checkAndGroup(items[i],filters)&&retItems.push(items[i]);return retItems}function checkStatement(item,statement){if(angular.isFunction(statement))return statement(item);var STATEMENT_LENGTH=3;if(statement.lengthA lightweight AngularJS dynamic grid layout
+ ++ The items provided are displayed by placing elements in optimal position based on available vertical space. +
+
+    
+  
+
diff --git a/src/js/dynamic-layout-item.directive.js b/src/js/dynamic-layout-item.directive.js
new file mode 100644
index 0000000..06830ec
--- /dev/null
+++ b/src/js/dynamic-layout-item.directive.js
@@ -0,0 +1,87 @@
+(function() {
+  'use strict';
+
+  angular
+    .module('dynamicLayout')
+    .directive('dynamicLayoutItem', dynamicLayoutItem);
+
+  /* @ngInject */
+  function dynamicLayoutItem($window, $animate) {
+    return {
+      restrict: 'A',
+      require: '^dynamicLayout',
+      link: link
+    };
+
+    function link(scope, element, attrs, ctrl) {
+
+      var animation;
+
+      scope.dimensions = {
+        columnSpan: 0,
+        width: 0,
+        height: 0
+      };
+      scope.pos = {
+        x: element[0].offsetLeft,
+        y: element[0].offsetTop
+      };
+      scope.calculateDimensions = calculateDimensions;
+
+      ctrl.subscribe(scope);
+
+      scope.$watch('$index', function(newVal, oldVal) {
+        if (newVal !== oldVal) {
+          ctrl.layout();
+        }
+      });
+      scope.$watchCollection('pos', function(newPos, oldPos) {
+        position(newPos, oldPos);
+      });
+
+      // Cleanup
+      scope.$on('$destroy', function() {
+        ctrl.unsubscribe(scope);
+      });
+
+      function calculateDimensions() {
+        var rect = element[0].getBoundingClientRect();
+        var style = $window.getComputedStyle(element[0]);
+        var width;
+        var height;
+
+        if (rect.width) {
+          width = rect.width;
+          height = rect.height;
+        } else {
+          width = rect.right - rect.left;
+          height = rect.top - rect.bottom;
+        }
+
+        scope.dimensions.width = width + parseFloat(style.marginLeft) + parseFloat(style.marginRight);
+        scope.dimensions.height = height + parseFloat(style.marginTop) + parseFloat(style.marginBottom);
+      }
+
+      function position(newPos, oldPos) {
+        if (animation) {
+          $animate.cancel(animation);
+        }
+        animation = $animate.addClass(element, 'move-items-animation', {
+          from: {
+            position: 'absolute',
+            left: oldPos.x + 'px',
+            top: oldPos.y + 'px'
+          },
+          to: {
+            left: newPos.x + 'px',
+            top: newPos.y + 'px'
+          }
+        }).then(function() {
+          element.removeClass('move-items-animation');
+        });
+      }
+
+    }
+  }
+
+})();
diff --git a/src/js/dynamic-layout.directive.js b/src/js/dynamic-layout.directive.js
new file mode 100644
index 0000000..0c9a422
--- /dev/null
+++ b/src/js/dynamic-layout.directive.js
@@ -0,0 +1,76 @@
+(function() {
+  'use strict';
+
+  angular
+    .module('dynamicLayout')
+    .directive('dynamicLayout', dynamicLayout);
+
+  /*
+   * The isotope directive that renders the templates based on the array of items
+   * passed
+   *
+   * @ngInject
+   */
+  function dynamicLayout($window, $timeout, PositionService) {
+
+    return {
+      restrict: 'A',
+      controller: controller
+    };
+
+    function controller($scope, $element) {
+
+      var vm = this;
+      var timeoutId;
+      var items = [];
+
+      vm.subscribe = subscribe;
+      vm.unsubscribe = unsubscribe;
+      vm.layout = layout;
+
+      /*
+       * Triggers a layout every time the window is resized
+       */
+      angular.element($window).on('resize', layout);
+
+      $scope.$on('$destroy', function() {
+        angular.element($window).off('resize', layout);
+      });
+
+      function subscribe(item) {
+        items.push(item);
+        layout();
+      }
+
+      function unsubscribe(item) {
+        items.splice(items.indexOf(item), 1);
+        layout();
+      }
+
+      function layout() {
+        $timeout.cancel(timeoutId);
+        timeoutId = $timeout(function() {
+
+          if (!items.length) {
+            return;
+          }
+          
+          items.sort(function(a, b) {
+            if (a.$index < b.$index) {
+              return -1;
+            } else if (a.$index > b.$index) {
+              return 1;
+            }
+            return 0;
+          });
+
+          var lastItem = items[items.length - 1];
+          PositionService.layout($element, items);
+          $element[0].style.height = lastItem.pos.y + lastItem.dimensions.height + 'px';
+        });
+      }
+
+    }
+  }
+
+})();
diff --git a/src/js/layout-on-click.directive.js b/src/js/layout-on-click.directive.js
new file mode 100644
index 0000000..cbf4657
--- /dev/null
+++ b/src/js/layout-on-click.directive.js
@@ -0,0 +1,28 @@
+(function() {
+  'use strict';
+
+  angular
+    .module('dynamicLayout')
+    .directive('layoutOnClick', layoutOnClick);
+
+  /*
+   * Directive on images to layout after each load
+   *
+   * @ngInject
+   */
+  function layoutOnClick() {
+
+    return {
+      restrict: 'A',
+      require: '^dynamicLayout',
+      link: link
+    };
+
+    function link(scope, element, attrs, ctrl) {
+      element.on('click', function() {
+        ctrl.layout();
+      });
+    }
+  }
+
+})();
diff --git a/src/js/layout-on-load.directive.js b/src/js/layout-on-load.directive.js
new file mode 100644
index 0000000..f6f3c55
--- /dev/null
+++ b/src/js/layout-on-load.directive.js
@@ -0,0 +1,28 @@
+(function() {
+  'use strict';
+
+  angular
+    .module('dynamicLayout')
+    .directive('layoutOnLoad', layoutOnLoad);
+
+  /*
+   * Directive on images to layout after each load
+   *
+   * @ngInject
+   */
+  function layoutOnLoad() {
+
+    return {
+      restrict: 'A',
+      require: '^dynamicLayout',
+      link: link
+    };
+
+    function link(scope, element, attrs, ctrl) {
+      element.on('load error', function() {
+        ctrl.layout();
+      });
+    }
+  }
+
+})();
diff --git a/src/module.js b/src/js/module.js
similarity index 100%
rename from src/module.js
rename to src/js/module.js
diff --git a/src/js/position.service.js b/src/js/position.service.js
new file mode 100644
index 0000000..cade493
--- /dev/null
+++ b/src/js/position.service.js
@@ -0,0 +1,164 @@
+(function() {
+  'use strict';
+
+  angular
+    .module('dynamicLayout')
+    .factory('PositionService', PositionService);
+
+  /*
+   * The position service
+   *
+   * Find the best adjustements of the elemnts in the DOM according the their
+   * order, height and width
+   *
+   * Fix their absolute position in the DOM while adding a ng-animate class for
+   * personalized animations
+   *
+   * @ngInject
+   */
+  function PositionService() {
+
+    return {
+      layout: layout
+    };
+
+    function layout(element, items) {
+
+      // Calculate dimensions
+      angular.forEach(items, function(item) {
+        item.calculateDimensions();
+      });
+
+      // 2) Calculate amount of columns using total width and item width
+      var colWidth = getColWidth(items);
+
+      // Apply columnSpan to each item
+      angular.forEach(items, function(item) {
+        item.dimensions.columnSpan = Math.round(item.dimensions.width / colWidth);
+      });
+
+      // We set what should be their absolute position in the DOM
+      return setItemsPosition(element[0].offsetWidth, colWidth, items);
+    }
+
+    /*
+     * Get the column size based on the minimum width of the items
+     * @return: column size
+     */
+    function getColWidth(items) {
+      var colWidth;
+      angular.forEach(items, function(item) {
+        if (!colWidth || item.dimensions.width < colWidth) {
+          colWidth = item.dimensions.width;
+        }
+      });
+      return colWidth;
+    }
+
+    /*
+     * Set the items' absolute position
+     * @param columns: the empty columns
+     * @param colWidth: the column size
+     */
+    function setItemsPosition(containerWidth, colWidth, items) {
+
+      var columns = initColumns(containerWidth, colWidth);
+
+      angular.forEach(items, function(item) {
+        var columnHeights = getColumnHeights(columns);
+        var colPos = getItemColumnsAndPosition(item, columnHeights, colWidth);
+        var j;
+
+        for (j in colPos.columns) {
+          columns[colPos.columns[j]].push(item);
+        }
+
+        item.pos.x = colPos.position.x;
+        item.pos.y = colPos.position.y;
+      });
+    }
+
+    /*
+     * Intialize the columns
+     * @param nb: the number of columns to be initialized
+     * @return: the empty columns
+     */
+    function initColumns(containerWidth, colWidth) {
+      var amount = Math.round(containerWidth / colWidth);
+      var columns = [];
+      var i;
+      for (i = 0; i < amount; ++i) {
+        columns.push([]);
+      }
+      return columns;
+    }
+
+    /*
+     * Get the columns heights
+     * @param columns: the columns with the items they contain
+     * @return: an array of columns heights
+     */
+    function getColumnHeights(columns) {
+      var columnHeights = [];
+      var i;
+      for (i in columns) {
+        var h = 0;
+        if (columns[i].length) {
+          var lastItem = columns[i][columns[i].length - 1];
+          h = lastItem.pos.y + lastItem.dimensions.height;
+        }
+        columnHeights.push(h);
+      }
+      return columnHeights;
+    }
+
+    /*
+     * Find the item absolute position and what columns it belongs too
+     * @param item: the item to place
+     * @param colHeights: the current height of the column when all items prior
+     * to this one were placed
+     * @param colWidth: the column size
+     * @return the item's columms and coordinates
+     */
+    function getItemColumnsAndPosition(item, colHeights, colWidth) {
+      if (item.dimensions.columnSpan > colHeights.length) {
+        throw 'Item too large';
+      }
+
+      var indexOfMin = 0;
+      var minFound = 0;
+      var i;
+
+      // We look at what set of columns have the minimum height
+      for (i = 0; i <= colHeights.length - item.dimensions.columnSpan; ++i) {
+        var startingColumn = i;
+        var endingColumn = i + item.dimensions.columnSpan;
+        var maxHeightInPart = Math.max.apply(
+          Math, colHeights.slice(startingColumn, endingColumn)
+        );
+
+        if (i === 0 || maxHeightInPart < minFound) {
+          minFound = maxHeightInPart;
+          indexOfMin = i;
+        }
+      }
+
+      var itemColumns = [];
+      for (i = indexOfMin; i < indexOfMin + item.dimensions.columnSpan; ++i) {
+        itemColumns.push(i);
+      }
+
+      var position = {
+        x: itemColumns[0] * colWidth,
+        y: minFound
+      };
+
+      return {
+        columns: itemColumns,
+        position: position
+      };
+    }
+
+  }
+
+})();
diff --git a/src/layout-on-load.directive.js b/src/layout-on-load.directive.js
deleted file mode 100644
index d734050..0000000
--- a/src/layout-on-load.directive.js
+++ /dev/null
@@ -1,23 +0,0 @@
-(function() {
-  'use strict';
-
-  angular
-    .module('dynamicLayout')
-    .directive('layoutOnLoad', ['$rootScope', layoutOnLoad]);
-
-  /*
-   * Directive on images to layout after each load
-   */
-  function layoutOnLoad($rootScope) {
-
-    return {
-      restrict: 'A',
-      link: function(scope, element) {
-        element.bind('load error', function() {
-          $rootScope.$broadcast('dynamicLayout.layout');
-        });
-      }
-    };
-  }
-
-})();
diff --git a/src/partials/aboutMe.html b/src/partials/aboutMe.html
new file mode 100755
index 0000000..06460fb
--- /dev/null
+++ b/src/partials/aboutMe.html
@@ -0,0 +1,19 @@
+    Name
+Occupation
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mauris tellus, vehicula ut tellus id, suscipit dapibus tortor. Integer viverra turpis ac fringilla hendrerit. Sed faucibus posuere felis et pellentesque. Cras varius tortor vitae molestie tempor. Proin ut viverra elit, ac gravida tortor.
+Degree
+Field of Study
+School
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mauris tellus, vehicula ut tellus id, suscipit dapibus tortor. Integer viverra turpis ac fringilla hendrerit. Sed faucibus posuere felis et pellentesque. Cras varius tortor vitae molestie tempor. Proin ut viverra elit, ac gravida tortor.
+Position
+Company
++ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mauris tellus, vehicula ut tellus id, suscipit dapibus tortor. Integer viverra turpis ac fringilla hendrerit. Sed faucibus posuere felis et pellentes ... + + que let me illustrate how an item expands and how other items rearrange around it. + Cras varius tortor vitae molestie tempor. Proin ut viverra elit, ac gravida tortor. + +
+International Office
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mauris tellus, vehicula ut tellus id, suscipit dapibus tortor. Integer viverra turpis ac fringilla hendrerit. Sed faucibus posuere felis et pellentesque. Cras varius tortor vitae molestie tempor. Proin ut viverra elit, ac gravida tortor.
+Another example of card to add to your profile
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mauris tellus, vehicula ut tellus id, suscipit dapibus tortor. Integer viverra turpis ac fringilla hendrerit. Sed faucibus posuere felis et pellentesque. Cras varius tortor vitae molestie tempor. Proin ut viverra elit, ac gravida tortor.
+