├── .editorconfig
├── .eslintrc
├── .gitignore
├── Gruntfile.js
├── README.md
├── bower.json
├── dist
├── angular-dynamic-layout.js
└── angular-dynamic-layout.min.js
├── karma.conf.js
├── package.json
├── src
├── as.filter.js
├── custom-filter.filter.js
├── custom-ranker.filter.js
├── dynamic-layout.directive.js
├── filter.service.js
├── layout-on-load.directive.js
├── module.js
├── position.service.js
└── ranker.service.js
└── tests
├── .eslintrc
├── filter.spec.js
├── position.spec.js
└── ranker.spec.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.json]
13 | indent_size = 2
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "plugins": [
6 | "angular"
7 | ],
8 | "rules": {
9 | "brace-style": [1, "1tbs"],
10 | "camelcase": [1, {"properties": "never"}],
11 | "consistent-return": 1,
12 | "indent": [2, 2],
13 | "new-cap": 0,
14 | "no-array-constructor": 2,
15 | "no-extra-parens": 1,
16 | "no-new-object": 2,
17 | "no-use-before-define": 0,
18 | "no-underscore-dangle": 0,
19 | "no-unexpected-multiline": 2,
20 | "no-unused-vars": 1,
21 | "one-var": [1, "never"],
22 | "quotes": [1, "single"],
23 | "space-after-keywords": 1,
24 | "space-before-blocks": 1,
25 | "space-before-function-paren": [1, "never"],
26 | "space-infix-ops": 1,
27 | "valid-jsdoc": 1
28 | },
29 | "globals": {
30 | "angular": true,
31 | "_": true,
32 | "moment": true,
33 | "S": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bower_components
3 | .idea
4 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | // Gruntfile.js
2 |
3 | // our wrapper function (required by grunt and its plugins)
4 | // all configuration goes inside this function
5 | module.exports = function (grunt) {
6 |
7 | // ===========================================================================
8 | // CONFIGURE GRUNT ===========================================================
9 | // ===========================================================================
10 | grunt.initConfig({
11 |
12 | // get the configuration info from package.json ----------------------------
13 | // this way we can use things like name and version (pkg.name)
14 | pkg: grunt.file.readJSON('package.json'),
15 | concat: {
16 | options: {
17 | separator: ';'
18 | },
19 | dist: {
20 | src: ['src/module.js',
21 | 'src/dynamic-layout.directive.js',
22 | 'src/layout-on-load.directive.js',
23 | 'src/filter.service.js',
24 | 'src/position.service.js',
25 | 'src/ranker.service.js',
26 | 'src/as.filter.js',
27 | 'src/custom-filter.filter.js',
28 | 'src/custom-ranker.filter.js'],
29 | dest: 'dist/<%= pkg.name %>.js'
30 | }
31 | },
32 | karma: {
33 | unit: {
34 | configFile: 'karma.conf.js',
35 | singleRun: true
36 | }
37 | },
38 | jshint: {
39 | // when this task is run, lint the Gruntfile and all js files in src
40 | options: {
41 | multistr: true,
42 | },
43 | build: ['Grunfile.js', 'src/**/*.js', 'tests/**/*.js']
44 | },
45 | uglify: {
46 | options: {
47 | banner: '/*\n <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> \n*/\n',
48 | mangle: false
49 | },
50 | build: {
51 | files: {
52 | 'dist/<%= pkg.name %>.min.js': ['dist/<%= pkg.name %>.js'],
53 | }
54 | }
55 | }
56 | });
57 |
58 | // ===========================================================================
59 | // LOAD GRUNT PLUGINS ========================================================
60 | // ===========================================================================
61 | // we can only load these if they are in our package.json
62 | // make sure you have run npm install so our app can find these
63 | grunt.loadNpmTasks('grunt-karma');
64 | grunt.loadNpmTasks('grunt-contrib-jshint');
65 | grunt.loadNpmTasks('grunt-contrib-concat');
66 | grunt.loadNpmTasks('grunt-contrib-uglify');
67 | grunt.registerTask('default', ['karma', 'jshint', 'concat', 'uglify']);
68 | };
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [This project is looking for a collaborator to take over development as I no longer have the ressources to do so. If you have interested please let me know thanks ]
2 |
3 | # What is this?
4 |
5 | angular-dynamic-layout is an AngularJS dynamic grid layout. It is inspired from the successfull jQuery [isotope](http://isotope.metafizzy.co/) library and its underlying [masonry](http://masonry.desandro.com/) layout.
6 |
7 | Including jQuery isotope into AngularJS directives gets easily hacky, this library reproduces the layout behavior while taking advantage of `ngRepeat` filtering and ordering as well as `ngAnimate` module.
8 |
9 | It is meant to be a very light and customizable frame leaving a lot of freedom to the user, especially regarding templates, animations and overall design and responsiveness.
10 |
11 | This is a beta version and not production-ready.
12 |
13 | # Demo
14 | - [Demo](http://tristanguigue.github.io/angular-dynamic-layout)
15 | - [Demo Code](https://github.com/tristanguigue/angular-dynamic-layout/tree/gh-pages)
16 |
17 | # Installation
18 |
19 | ````
20 | bower install angular-dynamic-layout
21 | ```
22 | Or
23 | ````
24 | npm install angular-dynamic-layout
25 | ```
26 | # Usage
27 | Controller:
28 | ````
29 | $scope.cards = [
30 | {
31 | template : "app/partials/aboutMe.html",
32 | },
33 | {
34 | template : "app/partials/businessCard.html",
35 | }
36 | ];
37 | ````
38 | Template:
39 | ````
40 |
41 | ````
42 |
43 | ## Cards
44 |
45 | Items must have a template property, this template will be dynamically included using the `ng-include` directive.
46 |
47 | - Content: the templates' HTML content is entirely up to you.
48 |
49 | - Controller: each card can have a specific controller, you can also have a common controller for all cards. For example:
50 | ````
51 |
52 |
53 | My Card
54 |
55 | ````
56 |
57 | - External Scope: to reach the directive's parent controller you can use `externalScope()` in the cards' template.
58 |
59 | - Data: you can provide any data to your templates from the list of cards, for example:
60 |
61 | Contoller:
62 | ````
63 | $scope.cards = [
64 | {
65 | template : "app/partials/work1.html",
66 | tabs : ["home", "work"],
67 | data : {
68 | "position" : "Web Developer",
69 | "company" : "Hacker Inc."
70 | },
71 | added : 1414871272105,
72 | },
73 | {
74 | template : "app/partials/work1.html",
75 | tabs : ["home", "work"],
76 | data : {
77 | "position" : "Data Scientist",
78 | "company" : "Big Data Inc."
79 | },
80 | added : 1423871272105,
81 | }
82 | ];
83 | ````
84 | Template:
85 | ````
86 |
87 |
88 |
Position
89 |
{{it.data.position}}
90 |
91 |
92 |
Company
93 |
{{it.data.company}}
94 |
95 |
96 | ````
97 |
98 | ## Responsiveness
99 | To make your layout responsive just create a set of media queries that ajust the size of your cards and of the container. dynamic-layout will find the container and cards widths on its own. A layout will be triggered when the screen's size changes.
100 |
101 | ## Animations
102 | The animations are based on the `ngAnimate` module, they are entirely up to you and set in the CSS.
103 | Here are the available animations:
104 |
105 | - `move-items-animation`: use this class to animate the movement of the cards between two positions, for example:
106 | ````
107 | .move-items-animation{
108 | transition-property: left, top;
109 | transition-duration: 1s;
110 | transition-timing-function: ease-in-out;
111 | }
112 | ````
113 | - `ng-enter` and `ng-leave`: you can animate the entering and leaving of your cards in the grid, for example when applying filters:
114 |
115 | ````
116 | .dynamic-layout-item-parent.ng-enter{
117 | transition: .5s ease-in-out;
118 | opacity:0;
119 | }
120 | .dynamic-layout-item-parent.ng-enter.ng-enter-active{
121 | opacity:1;
122 | }
123 |
124 | .dynamic-layout-item-parent.ng-leave{
125 | transition: .5s ease-in-out;
126 | opacity:1;
127 | }
128 | .dynamic-layout-item-parent.ng-leave.ng-leave-active{
129 | opacity:0;
130 | }
131 | ````
132 |
133 | ## Features
134 |
135 | ### Filtering
136 | You can provide and update a list of filters like this:
137 | ````
138 |
139 | ````
140 | Those filters needs to be in the [Conjuctive Normal Form](http://en.wikipedia.org/wiki/Conjunctive_normal_form). Basically a list of and groups composed of or groups. Each statement contains the property to be evaluated, a comparator and the value(s) allowed. For example:
141 | ````
142 | var filters = [ // an AND group compose of OR groups
143 | [ // an OR group compose of statements
144 | ['color', '=', 'grey'], // A statement
145 | ['color', '=', 'black']
146 | ],
147 | [ // a second OR group composed of statements
148 | ['atomicNumber', '<', 3]
149 | ]
150 | ];
151 | ````
152 | The list of comparators available are:
153 | ````
154 | ['=', '<', '>', '<=', '>=', '!=', 'in', 'not in', 'contains']
155 | ````
156 | #### Custom filters
157 |
158 | You can make your own filters by providing any function that takes the item as input and returns a boolean. For example:
159 | ````
160 | var myCustomFilter = function(item){
161 | if(item.color != 'red')
162 | return true;
163 | else
164 | return false;
165 | };
166 |
167 | filters = [
168 | [myCustomFilter]
169 | ];
170 | ````
171 |
172 | ### Sorting
173 | You can provide and update a list of rankers like this:
174 | ````
175 |
176 | ````
177 | Each ranker contains the property to be evaluated and the order. If two items are the same regarding the first ranker, the second one is used to part them, etc.
178 | ````
179 | var rankers = [
180 | ["color", "asc"],
181 | ["atomicNumber", "desc"]
182 | ];
183 | ````
184 | #### Custom rankers
185 |
186 | You can make your own ranker by providing any function that takes the item as input and returns a value to be evaluated. For example:
187 |
188 | ````
189 | var myCustomGetter = function(item){
190 | if(item.atomicNumber > 5) return 1;
191 | else return 0;
192 | };
193 |
194 | rankers = [
195 | [myCustomGetter, "asc"]
196 | ];
197 | ````
198 |
199 | ### Adding and removing items
200 |
201 | You can add or remove any items from the cards list controller and the dynamicLayout directive will detect it. For example:
202 | ````
203 | $scope.cards.splice(index, 1);
204 | ````
205 |
206 | ### Triggering layout and callback
207 | If a card is modified in any way (expanded for example) you can trigger a layout by broacasting in the `$rootScope`. Once the animations are completed the callback will be executed.
208 |
209 | ````
210 | $scope.toggleText = function(){
211 | $scope.showingMoreText = !$scope.showingMoreText;
212 | // We need to broacast the layout on the next digest once the text
213 | // is actually shown
214 | $timeout(function(){
215 | $rootScope.$broadcast("layout", function(){
216 | // The layout animations have completed
217 | });
218 | });
219 | }
220 | ````
221 |
222 |
223 |
224 |
225 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-dynamic-layout",
3 | "version": "0.1.11",
4 | "main": "dist/angular-dynamic-layout.min.js",
5 | "dependencies": {
6 | "angular": "1.3.5",
7 | "angular-animate": "1.3.5"
8 | },
9 | "devDependencies": {
10 | "angular-mocks": "1.3.5"
11 | },
12 | "homepage": "https://github.com/tristanguigue/angular-dynamic-layout",
13 | "authors": [
14 | "Tristan Guigue "
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/tristanguigue/angular-dynamic-layout.git"
19 | },
20 | "description": "An angularJS approach to dynamic grid",
21 | "license": "MIT",
22 | "ignore": [
23 | "**/.*",
24 | "node_modules",
25 | "bower_components",
26 | "tests"
27 | ],
28 | "private": false
29 | }
30 |
--------------------------------------------------------------------------------
/dist/angular-dynamic-layout.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout', [ 'ngAnimate' ]);
6 |
7 | })();
8 | ;(function() {
9 | 'use strict';
10 |
11 | angular
12 | .module('dynamicLayout')
13 | .directive('dynamicLayout', ['$timeout', '$window', '$q', '$animate', 'PositionService', dynamicLayout]);
14 |
15 | /*
16 | * The isotope directive that renders the templates based on the array of items
17 | * passed
18 | * @scope items: the list of items to be rendered
19 | * @scope rankers: the rankers to be applied on the list of items
20 | * @scope filters: the filters to be applied on the list of items
21 | * @scope defaulttemplate: (optional) the deafult template to be applied on each item if no item template is defined
22 | */
23 | function dynamicLayout($timeout, $window, $q, $animate, PositionService) {
24 |
25 | return {
26 | restrict: 'A',
27 | scope: {
28 | items: '=',
29 | rankers: '=',
30 | filters: '=',
31 | defaulttemplate: '=?'
32 | },
33 | template: '',
41 | link: link
42 | };
43 |
44 | function link(scope, element) {
45 |
46 | // Keep count of the number of templates left to load
47 | scope.templatesToLoad = 0;
48 | scope.externalScope = externalScope;
49 |
50 | // Fires when a template is requested through the ng-include directive
51 | scope.$on('$includeContentRequested', function() {
52 | scope.templatesToLoad++;
53 | });
54 |
55 | // Fires when a template has been loaded through the ng-include
56 | // directive
57 | scope.$on('$includeContentLoaded', function() {
58 | scope.templatesToLoad--;
59 | });
60 |
61 | /*
62 | * Triggers a layout every time the items are changed
63 | */
64 | scope.$watch('filteredItems', function(newValue, oldValue) {
65 | // We want the filteredItems to be available to the controller
66 | // This feels hacky, there must be a better way to do this
67 | scope.$parent.filteredItems = scope.filteredItems;
68 |
69 | if (!angular.equals(newValue, oldValue)) {
70 | itemsLoaded().then(function() {
71 | layout();
72 | });
73 | }
74 | }, true);
75 |
76 | /*
77 | * Triggers a layout every time the window is resized
78 | */
79 | angular.element($window).bind('resize', function() {
80 | // We need to apply the scope
81 | scope.$apply(function() {
82 | layout();
83 | });
84 | });
85 |
86 | /*
87 | * Triggers a layout whenever requested by an external source
88 | * Allows a callback to be fired after the layout animation is
89 | * completed
90 | */
91 | scope.$on('layout', function(event, callback) {
92 | layout().then(function() {
93 | if (angular.isFunction('function')) {
94 | callback();
95 | }
96 | });
97 | });
98 |
99 | /*
100 | * Triggers the initial layout once all the templates are loaded
101 | */
102 | itemsLoaded().then(function() {
103 | layout();
104 | });
105 |
106 | /*
107 | * Use the PositionService to layout the items
108 | * @return the promise of the cards being animated
109 | */
110 | function layout() {
111 | return PositionService.layout(element[0].offsetWidth);
112 | }
113 |
114 | /*
115 | * Check when all the items have been loaded by the ng-include
116 | * directive
117 | */
118 | function itemsLoaded() {
119 | var def = $q.defer();
120 |
121 | // $timeout : We need to wait for the includeContentRequested to
122 | // be called before we can assume there is no templates to be loaded
123 | $timeout(function() {
124 | if (scope.templatesToLoad === 0) {
125 | def.resolve();
126 | }
127 | });
128 |
129 | scope.$watch('templatesToLoad', function(newValue, oldValue) {
130 | if (newValue !== oldValue && scope.templatesToLoad === 0) {
131 | def.resolve();
132 | }
133 | });
134 |
135 | return def.promise;
136 | }
137 |
138 | /*
139 | * This allows the external scope, that is the scope of
140 | * dynamic-layout's container to be called from the templates
141 | * @return the given scope
142 | */
143 | function externalScope() {
144 | return scope.$parent;
145 | }
146 |
147 | }
148 | }
149 |
150 | })();
151 | ;(function() {
152 | 'use strict';
153 |
154 | angular
155 | .module('dynamicLayout')
156 | .directive('layoutOnLoad', ['$rootScope', layoutOnLoad]);
157 |
158 | /*
159 | * Directive on images to layout after each load
160 | */
161 | function layoutOnLoad($rootScope) {
162 |
163 | return {
164 | restrict: 'A',
165 | link: function(scope, element) {
166 | element.bind('load error', function() {
167 | $rootScope.$broadcast('layout');
168 | });
169 | }
170 | };
171 | }
172 |
173 | })();
174 | ;(function() {
175 | 'use strict';
176 |
177 | angular
178 | .module('dynamicLayout')
179 | .factory('FilterService', FilterService);
180 |
181 | /*
182 | * The filter service
183 | *
184 | * COMPARATORS = ['=', '<', '>', '<=', '>=', '!=', 'in', 'not in', 'contains']
185 | *
186 | * Allows filters in Conjuctive Normal Form using the item's property or any
187 | * custom operation on the items
188 | *
189 | * For example:
190 | * var filters = [ // an AND goup compose of OR groups
191 | * [ // an OR group compose of statements
192 | * ['color', '=', 'grey'], // A statement
193 | * ['color', '=', 'black']
194 | * ],
195 | * [ // a second OR goup composed of statements
196 | * ['atomicNumber', '<', 3]
197 | * ]
198 | * ];
199 | * Or
200 | * var myCustomFilter = function(item){
201 | * if(item.color != 'red')
202 | * return true;
203 | * else
204 | * return false;
205 | * };
206 | *
207 | * filters = [
208 | * [myCustomFilter]
209 | * ];
210 | *
211 | */
212 | function FilterService() {
213 |
214 | return {
215 | applyFilters: applyFilters
216 | };
217 |
218 | /*
219 | * Check which of the items passes the filters
220 | * @param items: the items being probed
221 | * @param filters: the array of and groups use to probe the item
222 | * @return the list of items that passes the filters
223 | */
224 | function applyFilters(items, filters) {
225 | var retItems = [];
226 | var i;
227 | for (i in items) {
228 | if (checkAndGroup(items[i], filters)) {
229 | retItems.push(items[i]);
230 | }
231 | }
232 | return retItems;
233 | }
234 |
235 | /*
236 | * Check if a single item passes the single statement criteria
237 | * @param item: the item being probed
238 | * @param statement: the criteria being use to test the item
239 | * @return true if the item passed the statement, false otherwise
240 | */
241 | function checkStatement(item, statement) {
242 | // If the statement is a custom filter, we give the item as a parameter
243 | if (angular.isFunction(statement)) {
244 | return statement(item);
245 | }
246 |
247 | // If the statement is a regular filter, it has to be with the form
248 | // [propertyName, comparator, value]
249 |
250 | var STATEMENT_LENGTH = 3;
251 | if (statement.length < STATEMENT_LENGTH) {
252 | throw 'Incorrect statement';
253 | }
254 |
255 | var property = statement[0];
256 | var comparator = statement[1];
257 | var value = statement[2];
258 |
259 | // If the property is not found in the item then we consider the
260 | // statement to be false
261 | if (!item[property]) {
262 | return false;
263 | }
264 |
265 | switch (comparator) {
266 | case '=':
267 | return item[property] === value;
268 | case '<':
269 | return item[property] < value;
270 | case '<=':
271 | return item[property] <= value;
272 | case '>':
273 | return item[property] > value;
274 | case '>=':
275 | return item[property] >= value;
276 | case '!=':
277 | return item[property] !== value;
278 | case 'in':
279 | return item[property] in value;
280 | case 'not in':
281 | return !(item[property] in value);
282 | case 'contains':
283 | if (!(item[property] instanceof Array)) {
284 | throw 'contains statement has to be applied on array';
285 | }
286 | return item[property].indexOf(value) > -1;
287 | default:
288 | throw 'Incorrect statement comparator: ' + comparator;
289 | }
290 | }
291 |
292 | /*
293 | * Check a sub (or) group
294 | * @param item: the item being probed
295 | * @param orGroup: the array of statement use to probe the item
296 | * @return true if the item passed at least one of the statements,
297 | * false otherwise
298 | */
299 | function checkOrGroup(item, orGroup) {
300 | var j;
301 | for (j in orGroup) {
302 | if (checkStatement(item, orGroup[j])) {
303 | return true;
304 | }
305 | }
306 | return false;
307 | }
308 |
309 | /*
310 | * Check the main group
311 | * @param item: the item being probed
312 | * @param orGroup: the array of or groups use to probe the item
313 | * @return true if the item passed all of of the or groups,
314 | * false otherwise
315 | */
316 | function checkAndGroup(item, andGroup) {
317 | var i;
318 | for (i in andGroup) {
319 | if (!checkOrGroup(item, andGroup[i])) {
320 | return false;
321 | }
322 | }
323 | return true;
324 | }
325 |
326 | }
327 |
328 | })();
329 | ;(function() {
330 | 'use strict';
331 |
332 | angular
333 | .module('dynamicLayout')
334 | .factory('PositionService', ['$window', '$document', '$animate', '$timeout', '$q', PositionService]);
335 |
336 | /*
337 | * The position service
338 | *
339 | * Find the best adjustements of the elemnts in the DOM according the their
340 | * order, height and width
341 | *
342 | * Fix their absolute position in the DOM while adding a ng-animate class for
343 | * personalized animations
344 | *
345 | */
346 | function PositionService($window, $document, $animate, $timeout, $q) {
347 |
348 | // The list of ongoing animations
349 | var ongoingAnimations = {};
350 | // The list of items related to the DOM elements
351 | var items = [];
352 | // The list of the DOM elements
353 | var elements = [];
354 | // The columns that contains the items
355 | var columns = [];
356 |
357 | var self = {
358 | getItemsDimensionFromDOM: getItemsDimensionFromDOM,
359 | applyToDOM: applyToDOM,
360 | layout: layout,
361 | getColumns: getColumns
362 | };
363 | return self;
364 |
365 | /*
366 | * Get the items heights and width from the DOM
367 | * @return: the list of items with their sizes
368 | */
369 | function getItemsDimensionFromDOM() {
370 | // not(.ng-leave) : we don't want to select elements that have been
371 | // removed but are still in the DOM
372 | elements = $document[0].querySelectorAll(
373 | '.dynamic-layout-item-parent:not(.ng-leave)'
374 | );
375 | items = [];
376 | for (var i = 0; i < elements.length; ++i) {
377 | // Note: we need to get the children element width because that's
378 | // where the style is applied
379 | var rect = elements[i].children[0].getBoundingClientRect();
380 | var width;
381 | var height;
382 | if (rect.width) {
383 | width = rect.width;
384 | height = rect.height;
385 | } else {
386 | width = rect.right - rect.left;
387 | height = rect.top - rect.bottom;
388 | }
389 |
390 | items.push({
391 | height: height +
392 | parseFloat($window.getComputedStyle(elements[i]).marginTop),
393 | width: width +
394 | parseFloat(
395 | $window.getComputedStyle(elements[i].children[0]).marginLeft
396 | )
397 | });
398 | }
399 | return items;
400 | }
401 |
402 | /*
403 | * Apply positions to the DOM with an animation
404 | * @return: the promise of the position animations being completed
405 | */
406 | function applyToDOM() {
407 |
408 | var ret = $q.defer();
409 |
410 | /*
411 | * Launch an animation on a specific element
412 | * Once the animation is complete remove it from the ongoing animation
413 | * @param element: the element being moved
414 | * @param i: the index of the current animation
415 | * @return: the promise of the animation being completed
416 | */
417 | function launchAnimation(element, i) {
418 | var animationPromise = $animate.addClass(element,
419 | 'move-items-animation',
420 | {
421 | from: {
422 | position: 'absolute'
423 | },
424 | to: {
425 | left: items[i].x + 'px',
426 | top: items[i].y + 'px'
427 | }
428 | }
429 | );
430 |
431 | animationPromise.then(function() {
432 | // We remove the class so that the animation can be ran again
433 | element.classList.remove('move-items-animation');
434 | delete ongoingAnimations[i];
435 | });
436 |
437 | return animationPromise;
438 | }
439 |
440 | /*
441 | * Launch the animations on all the elements
442 | * @return: the promise of the animations being completed
443 | */
444 | function launchAnimations() {
445 | var i;
446 | for (i = 0; i < items.length; ++i) {
447 | // We need to pass the specific element we're dealing with
448 | // because at the next iteration elements[i] might point to
449 | // something else
450 | ongoingAnimations[i] = launchAnimation(elements[i], i);
451 | }
452 | $q.all(ongoingAnimations).then(function() {
453 | ret.resolve();
454 | });
455 | }
456 |
457 | // We need to cancel all ongoing animations before we start the new
458 | // ones
459 | if (Object.keys(ongoingAnimations).length) {
460 | for (var j in ongoingAnimations) {
461 | $animate.cancel(ongoingAnimations[j]);
462 | delete ongoingAnimations[j];
463 | }
464 | }
465 |
466 | // For some reason we need to launch the new animations at the next
467 | // digest
468 | $timeout(function() {
469 | launchAnimations(ret);
470 | });
471 |
472 | return ret.promise;
473 | }
474 |
475 | /*
476 | * Apply the position service on the elements in the DOM
477 | * @param containerWidth: the width of the dynamic-layout container
478 | * @return: the promise of the position animations being completed
479 | */
480 | function layout(containerWidth) {
481 | // We first gather the items dimension based on the DOM elements
482 | items = self.getItemsDimensionFromDOM();
483 |
484 | // Then we get the column size base the elements minimum width
485 | var colSize = getColSize();
486 | var nbColumns = Math.floor(containerWidth / colSize);
487 | // We create empty columns to be filled with the items
488 | initColumns(nbColumns);
489 |
490 | // We determine what is the column size of each of the items based on
491 | // their width and the column size
492 | setItemsColumnSpan(colSize);
493 |
494 | // We set what should be their absolute position in the DOM
495 | setItemsPosition(columns, colSize);
496 |
497 | // We apply those positions to the DOM with an animation
498 | return self.applyToDOM();
499 | }
500 |
501 | // Make the columns public
502 | function getColumns() {
503 | return columns;
504 | }
505 |
506 | /*
507 | * Intialize the columns
508 | * @param nb: the number of columns to be initialized
509 | * @return: the empty columns
510 | */
511 | function initColumns(nb) {
512 | columns = [];
513 | var i;
514 | for (i = 0; i < nb; ++i) {
515 | columns.push([]);
516 | }
517 | return columns;
518 | }
519 |
520 | /*
521 | * Get the columns heights
522 | * @param columns: the columns with the items they contain
523 | * @return: an array of columns heights
524 | */
525 | function getColumnsHeights(cols) {
526 | var columnsHeights = [];
527 | var i;
528 | for (i in cols) {
529 | var h;
530 | if (cols[i].length) {
531 | var lastItem = cols[i][cols[i].length - 1];
532 | h = lastItem.y + lastItem.height;
533 | } else {
534 | h = 0;
535 | }
536 | columnsHeights.push(h);
537 | }
538 | return columnsHeights;
539 | }
540 |
541 | /*
542 | * Find the item absolute position and what columns it belongs too
543 | * @param item: the item to place
544 | * @param colHeights: the current heigh of the column when all items prior to this
545 | * one were places
546 | * @param colSize: the column size
547 | * @return the item's columms and coordinates
548 | */
549 | function getItemColumnsAndPosition(item, colHeights, colSize) {
550 | if (item.columnSpan > colHeights.length) {
551 | throw 'Item too large';
552 | }
553 |
554 | var indexOfMin = 0;
555 | var minFound = 0;
556 | var i;
557 |
558 | // We look at what set of columns have the minimum height
559 | for (i = 0; i <= colHeights.length - item.columnSpan; ++i) {
560 | var startingColumn = i;
561 | var endingColumn = i + item.columnSpan;
562 | var maxHeightInPart = Math.max.apply(
563 | Math, colHeights.slice(startingColumn, endingColumn)
564 | );
565 |
566 | if (i === 0 || maxHeightInPart < minFound) {
567 | minFound = maxHeightInPart;
568 | indexOfMin = i;
569 | }
570 | }
571 |
572 | var itemColumns = [];
573 | for (i = indexOfMin; i < indexOfMin + item.columnSpan; ++i) {
574 | itemColumns.push(i);
575 | }
576 |
577 | var position = {
578 | x: itemColumns[0] * colSize,
579 | y: minFound
580 | };
581 |
582 | return {
583 | columns: itemColumns,
584 | position: position
585 | };
586 | }
587 |
588 | /*
589 | * Set the items' absolute position
590 | * @param columns: the empty columns
591 | * @param colSize: the column size
592 | */
593 | function setItemsPosition(cols, colSize) {
594 | var i;
595 | var j;
596 | for (i = 0; i < items.length; ++i) {
597 | var columnsHeights = getColumnsHeights(cols);
598 |
599 | var itemColumnsAndPosition = getItemColumnsAndPosition(items[i],
600 | columnsHeights,
601 | colSize);
602 |
603 | // We place the item in the found columns
604 | for (j in itemColumnsAndPosition.columns) {
605 | columns[itemColumnsAndPosition.columns[j]].push(items[i]);
606 | }
607 |
608 | items[i].x = itemColumnsAndPosition.position.x;
609 | items[i].y = itemColumnsAndPosition.position.y;
610 | }
611 | }
612 |
613 | /*
614 | * Get the column size based on the minimum width of the items
615 | * @return: column size
616 | */
617 | function getColSize() {
618 | var colSize;
619 | var i;
620 | for (i = 0; i < items.length; ++i) {
621 | if (!colSize || items[i].width < colSize) {
622 | colSize = items[i].width;
623 | }
624 | }
625 | return colSize;
626 | }
627 |
628 | /*
629 | * Set the column span for each of the items based on their width and the
630 | * column size
631 | * @param: column size
632 | */
633 | function setItemsColumnSpan(colSize) {
634 | var i;
635 | for (i = 0; i < items.length; ++i) {
636 | items[i].columnSpan = Math.ceil(items[i].width / colSize);
637 | }
638 | }
639 |
640 | }
641 |
642 | })();
643 | ;(function() {
644 | 'use strict';
645 |
646 | angular
647 | .module('dynamicLayout')
648 | .factory('RankerService', RankerService);
649 |
650 | /*
651 | * The rankers service
652 | *
653 | * Allows a list of rankers to sort the items.
654 | * If two items are the same regarding the first ranker, the second one is used
655 | * to part them, etc.
656 | *
657 | * Rankers can be either a property name or a custom operation on the item.
658 | * They all need to specify the order chosen (asc' or 'desc')
659 | *
660 | * var rankers = [
661 | * ['color', 'asc'],
662 | * ['atomicNumber', 'desc']
663 | * ];
664 | * Or
665 | * var rankers = [
666 | * [myCustomGetter, 'asc']
667 | * ];
668 | *
669 | */
670 | function RankerService() {
671 |
672 | return {
673 | applyRankers: applyRankers
674 | };
675 |
676 | /*
677 | * Order the items with the given rankers
678 | * @param items: the items being ranked
679 | * @param rankers: the array of rankers used to rank the items
680 | * @return the ordered list of items
681 | */
682 | function applyRankers(items, rankers) {
683 | // The ranker counter
684 | var i = 0;
685 |
686 | if (rankers) {
687 | items.sort(sorter);
688 | }
689 |
690 | /*
691 | * The custom sorting function using the built comparison function
692 | * @param a, b: the items to be compared
693 | * @return -1, 0 or 1
694 | */
695 | function sorter(a, b) {
696 | i = 0;
697 | return recursiveRanker(a, b);
698 | }
699 |
700 | /*
701 | * Compare recursively two items
702 | * It first compare the items with the first ranker, if no conclusion
703 | * can be drawn it uses the second ranker and so on until it finds a
704 | * winner or there are no more rankers
705 | * @param a, b: the items to be compared
706 | * @return -1, 0 or 1
707 | */
708 | function recursiveRanker(a, b) {
709 | var ranker = rankers[i][0];
710 | var ascDesc = rankers[i][1];
711 | var valueA;
712 | var valueB;
713 | // If it is a custom ranker, give the item as input and gather the
714 | // ouput
715 | if (angular.isFunction(ranker)) {
716 | valueA = ranker(a);
717 | valueB = ranker(b);
718 | } else {
719 | // Otherwise use the item's properties
720 | if (!(ranker in a) && !(ranker in b)) {
721 | valueA = 0;
722 | valueB = 0;
723 | } else if (!(ranker in a)) {
724 | return ascDesc === 'asc' ? -1 : 1;
725 | } else if (!(ranker in b)) {
726 | return ascDesc === 'asc' ? 1 : -1;
727 | }
728 | valueA = a[ranker];
729 | valueB = b[ranker];
730 | }
731 |
732 | if (typeof valueA === typeof valueB) {
733 |
734 | if (angular.isString(valueA)) {
735 | var comp = valueA.localeCompare(valueB);
736 | if (comp === 1) {
737 | return ascDesc === 'asc' ? 1 : -1;
738 | } else if (comp === -1) {
739 | return ascDesc === 'asc' ? -1 : 1;
740 | }
741 | } else {
742 | if (valueA > valueB) {
743 | return ascDesc === 'asc' ? 1 : -1;
744 | } else if (valueA < valueB) {
745 | return ascDesc === 'asc' ? -1 : 1;
746 | }
747 | }
748 | }
749 |
750 | ++i;
751 |
752 | if (rankers.length > i) {
753 | return recursiveRanker(a, b);
754 | }
755 |
756 | return 0;
757 | }
758 |
759 | return items;
760 | }
761 |
762 | }
763 |
764 | })();
765 | ;(function() {
766 | 'use strict';
767 |
768 | angular
769 | .module('dynamicLayout')
770 | .filter('as', ['$parse', as]);
771 |
772 | /*
773 | * This allowed the result of the filters to be assigned to the scope
774 | */
775 | function as($parse) {
776 |
777 | return function(value, context, path) {
778 | $parse(path).assign(context, value);
779 | return value;
780 | };
781 | }
782 |
783 | })();
784 | ;(function() {
785 | 'use strict';
786 |
787 | angular
788 | .module('dynamicLayout')
789 | .filter('customFilter', ['FilterService', customFilter]);
790 |
791 | /*
792 | * The filter to be applied on the ng-repeat directive
793 | */
794 | function customFilter(FilterService) {
795 |
796 | return function(items, filters) {
797 | if (filters) {
798 | return FilterService.applyFilters(items, filters);
799 | }
800 | return items;
801 | };
802 | }
803 |
804 | })();
805 | ;(function() {
806 | 'use strict';
807 |
808 | angular
809 | .module('dynamicLayout')
810 | .filter('customRanker', ['RankerService', customRanker]);
811 |
812 | /*
813 | * The ranker to be applied on the ng-repeat directive
814 | */
815 | function customRanker(RankerService) {
816 |
817 | return function(items, rankers) {
818 | if (rankers) {
819 | return RankerService.applyRankers(items, rankers);
820 | }
821 | return items;
822 | };
823 | }
824 |
825 | })();
826 |
--------------------------------------------------------------------------------
/dist/angular-dynamic-layout.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | angular-dynamic-layout 2015-09-13
3 | */
4 | !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.length":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}}function checkOrGroup(item,orGroup){var j;for(j in orGroup)if(checkStatement(item,orGroup[j]))return!0;return!1}function checkAndGroup(item,andGroup){var i;for(i in andGroup)if(!checkOrGroup(item,andGroup[i]))return!1;return!0}return{applyFilters:applyFilters}}angular.module("dynamicLayout").factory("FilterService",FilterService)}(),function(){"use strict";function PositionService($window,$document,$animate,$timeout,$q){function getItemsDimensionFromDOM(){elements=$document[0].querySelectorAll(".dynamic-layout-item-parent:not(.ng-leave)"),items=[];for(var i=0;ii;++i)columns.push([]);return columns}function getColumnsHeights(cols){var i,columnsHeights=[];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}function getItemColumnsAndPosition(item,colHeights,colSize){if(item.columnSpan>colHeights.length)throw"Item too large";var i,indexOfMin=0,minFound=0;for(i=0;i<=colHeights.length-item.columnSpan;++i){var startingColumn=i,endingColumn=i+item.columnSpan,maxHeightInPart=Math.max.apply(Math,colHeights.slice(startingColumn,endingColumn));(0===i||minFound>maxHeightInPart)&&(minFound=maxHeightInPart,indexOfMin=i)}var itemColumns=[];for(i=indexOfMin;ivalueB)return"asc"===ascDesc?1:-1;if(valueB>valueA)return"asc"===ascDesc?-1:1}return++i,rankers.length>i?recursiveRanker(a,b):0}var i=0;return rankers&&items.sort(sorter),items}return{applyRankers:applyRankers}}angular.module("dynamicLayout").factory("RankerService",RankerService)}(),function(){"use strict";function as($parse){return function(value,context,path){return $parse(path).assign(context,value),value}}angular.module("dynamicLayout").filter("as",["$parse",as])}(),function(){"use strict";function customFilter(FilterService){return function(items,filters){return filters?FilterService.applyFilters(items,filters):items}}angular.module("dynamicLayout").filter("customFilter",["FilterService",customFilter])}(),function(){"use strict";function customRanker(RankerService){return function(items,rankers){return rankers?RankerService.applyRankers(items,rankers):items}}angular.module("dynamicLayout").filter("customRanker",["RankerService",customRanker])}();
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | // Karma configuration
3 | // Generated on Wed Aug 13 2014 10:18:58 GMT+0200 (CEST)
4 |
5 | module.exports = function(config) {
6 | config.set({
7 |
8 | // base path that will be used to resolve all patterns (eg. files, exclude)
9 | basePath: '',
10 |
11 |
12 | // frameworks to use
13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
14 | frameworks: ['jasmine'],
15 |
16 |
17 | // list of files / patterns to load in the browser
18 | files: [
19 | 'bower_components/angular/angular.min.js',
20 | 'bower_components/angular-animate/angular-animate.min.js',
21 | 'bower_components/angular-mocks/angular-mocks.js',
22 | 'src/module.js',
23 | 'src/as.filter.js',
24 | 'src/custom-filter.filter.js',
25 | 'src/custom-ranker.filter.js',
26 | 'src/dynamic-layout.directive.js',
27 | 'src/layout-on-load.directive.js',
28 | 'src/filter.service.js',
29 | 'src/ranker.service.js',
30 | 'src/position.service.js',
31 | 'tests/**/*.js'
32 | ],
33 |
34 |
35 | // list of files to exclude
36 | exclude: [
37 | ],
38 |
39 |
40 | // preprocess matching files before serving them to the browser
41 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
42 | preprocessors: {
43 | },
44 |
45 |
46 | // test results reporter to use
47 | // possible values: 'dots', 'progress'
48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
49 | reporters: ['progress'],
50 |
51 |
52 | // web server port
53 | port: 9876,
54 |
55 |
56 | // enable / disable colors in the output (reporters and logs)
57 | colors: true,
58 |
59 |
60 | // level of logging
61 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
62 | logLevel: config.LOG_INFO,
63 |
64 |
65 | // enable / disable watching file and executing tests whenever any file changes
66 | autoWatch: true,
67 |
68 |
69 | // start these browsers
70 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
71 | browsers: ['Firefox'],
72 |
73 |
74 | // Continuous Integration mode
75 | // if true, Karma captures browsers, runs the tests and exits
76 | singleRun: false
77 | });
78 | };
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-dynamic-layout",
3 | "version": "0.1.11",
4 | "description": "An angularJS approach to dynamic grid",
5 | "main": "index.js",
6 | "directories": {
7 | "doc": "doc"
8 | },
9 | "scripts": {
10 | "test": "karma"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git://github.com/tristanguigue/angular-dynamic-layout.git"
15 | },
16 | "devDependencies": {
17 | "bower": "~1.3.12",
18 | "grunt": "~0.4.5",
19 | "grunt-contrib-jshint": "latest",
20 | "grunt-contrib-uglify": "latest",
21 | "grunt-contrib-concat": "latest",
22 | "grunt-karma": "^0.9.0",
23 | "karma": "^0.12.24",
24 | "karma-chrome-launcher": "^0.1.5",
25 | "karma-firefox-launcher": "~0.1.3",
26 | "karma-jasmine": "~0.2.2",
27 | "karma-safari-launcher": "~0.1.1"
28 | },
29 | "author": "Tristan Guigue",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/tristanguigue/angular-dynamic-layout/issues"
33 | },
34 | "homepage": "https://github.com/tristanguigue/angular-dynamic-layout"
35 | }
36 |
--------------------------------------------------------------------------------
/src/as.filter.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .filter('as', ['$parse', as]);
7 |
8 | /*
9 | * This allowed the result of the filters to be assigned to the scope
10 | */
11 | function as($parse) {
12 |
13 | return function(value, context, path) {
14 | $parse(path).assign(context, value);
15 | return value;
16 | };
17 | }
18 |
19 | })();
20 |
--------------------------------------------------------------------------------
/src/custom-filter.filter.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .filter('customFilter', ['FilterService', customFilter]);
7 |
8 | /*
9 | * The filter to be applied on the ng-repeat directive
10 | */
11 | function customFilter(FilterService) {
12 |
13 | return function(items, filters) {
14 | if (filters) {
15 | return FilterService.applyFilters(items, filters);
16 | }
17 | return items;
18 | };
19 | }
20 |
21 | })();
22 |
--------------------------------------------------------------------------------
/src/custom-ranker.filter.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .filter('customRanker', ['RankerService', customRanker]);
7 |
8 | /*
9 | * The ranker to be applied on the ng-repeat directive
10 | */
11 | function customRanker(RankerService) {
12 |
13 | return function(items, rankers) {
14 | if (rankers) {
15 | return RankerService.applyRankers(items, rankers);
16 | }
17 | return items;
18 | };
19 | }
20 |
21 | })();
22 |
--------------------------------------------------------------------------------
/src/dynamic-layout.directive.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .directive('dynamicLayout', ['$timeout', '$window', '$q', '$animate', 'PositionService', dynamicLayout]);
7 |
8 | /*
9 | * The isotope directive that renders the templates based on the array of items
10 | * passed
11 | * @scope items: the list of items to be rendered
12 | * @scope rankers: the rankers to be applied on the list of items
13 | * @scope filters: the filters to be applied on the list of items
14 | * @scope defaulttemplate: (optional) the deafult template to be applied on each item if no item template is defined
15 | */
16 | function dynamicLayout($timeout, $window, $q, $animate, PositionService) {
17 |
18 | return {
19 | restrict: 'A',
20 | scope: {
21 | items: '=',
22 | rankers: '=',
23 | filters: '=',
24 | defaulttemplate: '=?',
25 | colWidth: '='
26 | },
27 | template: '',
35 | link: link
36 | };
37 |
38 | function link(scope, element) {
39 |
40 | // Keep count of the number of templates left to load
41 | scope.templatesToLoad = 0;
42 | scope.externalScope = externalScope;
43 |
44 | // Fires when a template is requested through the ng-include directive
45 | scope.$on('$includeContentRequested', function() {
46 | scope.templatesToLoad++;
47 | });
48 |
49 | // Fires when a template has been loaded through the ng-include
50 | // directive
51 | scope.$on('$includeContentLoaded', function() {
52 | scope.templatesToLoad--;
53 | });
54 |
55 | /*
56 | * Triggers a layout every time the items are changed
57 | */
58 | scope.$watch('filteredItems', function(newValue, oldValue) {
59 | // We want the filteredItems to be available to the controller
60 | // This feels hacky, there must be a better way to do this
61 | scope.$parent.filteredItems = scope.filteredItems;
62 |
63 | if (!angular.equals(newValue, oldValue)) {
64 | itemsLoaded().then(function() {
65 | layout();
66 | });
67 | }
68 | }, true);
69 |
70 | /*
71 | * Triggers a layout every time the window is resized
72 | */
73 | angular.element($window).on('resize', onResize);
74 |
75 | /*
76 | * Triggers a layout whenever requested by an external source
77 | * Allows a callback to be fired after the layout animation is
78 | * completed
79 | */
80 | scope.$on('dynamicLayout.layout', function(event, callback) {
81 | layout().then(function() {
82 | if (angular.isFunction('function')) {
83 | callback();
84 | }
85 | });
86 | });
87 |
88 | /*
89 | * Triggers the initial layout once all the templates are loaded
90 | */
91 | itemsLoaded().then(function() {
92 | layout();
93 | });
94 |
95 | // Cleanup
96 | scope.$on('$destroy', function() {
97 | angular.element($window).off('resize', onResize);
98 | });
99 |
100 | function onResize() {
101 | // We need to apply the scope
102 | scope.$apply(function() {
103 | layout();
104 | });
105 | }
106 |
107 | /*
108 | * Use the PositionService to layout the items
109 | * @return the promise of the cards being animated
110 | */
111 | function layout() {
112 | return PositionService.layout(element[0].offsetWidth, scope.colWidth);
113 | }
114 |
115 | /*
116 | * Check when all the items have been loaded by the ng-include
117 | * directive
118 | */
119 | function itemsLoaded() {
120 | var def = $q.defer();
121 |
122 | // $timeout : We need to wait for the includeContentRequested to
123 | // be called before we can assume there is no templates to be loaded
124 | $timeout(function() {
125 | if (scope.templatesToLoad === 0) {
126 | def.resolve();
127 | }
128 | });
129 |
130 | scope.$watch('templatesToLoad', function(newValue, oldValue) {
131 | if (newValue !== oldValue && scope.templatesToLoad === 0) {
132 | def.resolve();
133 | }
134 | });
135 |
136 | return def.promise;
137 | }
138 |
139 | /*
140 | * This allows the external scope, that is the scope of
141 | * dynamic-layout's container to be called from the templates
142 | * @return the given scope
143 | */
144 | function externalScope() {
145 | return scope.$parent;
146 | }
147 |
148 | }
149 | }
150 |
151 | })();
152 |
--------------------------------------------------------------------------------
/src/filter.service.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .factory('FilterService', FilterService);
7 |
8 | /*
9 | * The filter service
10 | *
11 | * COMPARATORS = ['=', '<', '>', '<=', '>=', '!=', 'in', 'not in', 'contains']
12 | *
13 | * Allows filters in Conjuctive Normal Form using the item's property or any
14 | * custom operation on the items
15 | *
16 | * For example:
17 | * var filters = [ // an AND goup compose of OR groups
18 | * [ // an OR group compose of statements
19 | * ['color', '=', 'grey'], // A statement
20 | * ['color', '=', 'black']
21 | * ],
22 | * [ // a second OR goup composed of statements
23 | * ['atomicNumber', '<', 3]
24 | * ]
25 | * ];
26 | * Or
27 | * var myCustomFilter = function(item){
28 | * if(item.color != 'red')
29 | * return true;
30 | * else
31 | * return false;
32 | * };
33 | *
34 | * filters = [
35 | * [myCustomFilter]
36 | * ];
37 | *
38 | */
39 | function FilterService() {
40 |
41 | return {
42 | applyFilters: applyFilters
43 | };
44 |
45 | /*
46 | * Check which of the items passes the filters
47 | * @param items: the items being probed
48 | * @param filters: the array of and groups use to probe the item
49 | * @return the list of items that passes the filters
50 | */
51 | function applyFilters(items, filters) {
52 | var retItems = [];
53 | var i;
54 | for (i in items) {
55 | if (checkAndGroup(items[i], filters)) {
56 | retItems.push(items[i]);
57 | }
58 | }
59 | return retItems;
60 | }
61 |
62 | /*
63 | * Check if a single item passes the single statement criteria
64 | * @param item: the item being probed
65 | * @param statement: the criteria being use to test the item
66 | * @return true if the item passed the statement, false otherwise
67 | */
68 | function checkStatement(item, statement) {
69 | // If the statement is a custom filter, we give the item as a parameter
70 | if (angular.isFunction(statement)) {
71 | return statement(item);
72 | }
73 |
74 | // If the statement is a regular filter, it has to be with the form
75 | // [propertyName, comparator, value]
76 |
77 | var STATEMENT_LENGTH = 3;
78 | if (statement.length < STATEMENT_LENGTH) {
79 | throw 'Incorrect statement';
80 | }
81 |
82 | var property = statement[0];
83 | var comparator = statement[1];
84 | var value = statement[2];
85 |
86 | // If the property is not found in the item then we consider the
87 | // statement to be false
88 | if (!item[property]) {
89 | return false;
90 | }
91 |
92 | switch (comparator) {
93 | case '=':
94 | return item[property] === value;
95 | case '<':
96 | return item[property] < value;
97 | case '<=':
98 | return item[property] <= value;
99 | case '>':
100 | return item[property] > value;
101 | case '>=':
102 | return item[property] >= value;
103 | case '!=':
104 | return item[property] !== value;
105 | case 'in':
106 | return item[property] in value;
107 | case 'not in':
108 | return !(item[property] in value);
109 | case 'contains':
110 | if (!(item[property] instanceof Array)) {
111 | throw 'contains statement has to be applied on array';
112 | }
113 | return item[property].indexOf(value) > -1;
114 | default:
115 | throw 'Incorrect statement comparator: ' + comparator;
116 | }
117 | }
118 |
119 | /*
120 | * Check a sub (or) group
121 | * @param item: the item being probed
122 | * @param orGroup: the array of statement use to probe the item
123 | * @return true if the item passed at least one of the statements,
124 | * false otherwise
125 | */
126 | function checkOrGroup(item, orGroup) {
127 | var j;
128 | for (j in orGroup) {
129 | if (checkStatement(item, orGroup[j])) {
130 | return true;
131 | }
132 | }
133 | return false;
134 | }
135 |
136 | /*
137 | * Check the main group
138 | * @param item: the item being probed
139 | * @param orGroup: the array of or groups use to probe the item
140 | * @return true if the item passed all of of the or groups,
141 | * false otherwise
142 | */
143 | function checkAndGroup(item, andGroup) {
144 | var i;
145 | for (i in andGroup) {
146 | if (!checkOrGroup(item, andGroup[i])) {
147 | return false;
148 | }
149 | }
150 | return true;
151 | }
152 |
153 | }
154 |
155 | })();
156 |
--------------------------------------------------------------------------------
/src/layout-on-load.directive.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .directive('layoutOnLoad', ['$rootScope', layoutOnLoad]);
7 |
8 | /*
9 | * Directive on images to layout after each load
10 | */
11 | function layoutOnLoad($rootScope) {
12 |
13 | return {
14 | restrict: 'A',
15 | link: function(scope, element) {
16 | element.bind('load error', function() {
17 | $timeout.cancel(timeoutId);
18 | timeoutId = $timeout(function() {
19 | $rootScope.$broadcast('dynamicLayout.layout');
20 | });
21 | });
22 | }
23 | };
24 | }
25 |
26 | })();
27 |
--------------------------------------------------------------------------------
/src/module.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout', [ 'ngAnimate' ]);
6 |
7 | })();
8 |
--------------------------------------------------------------------------------
/src/position.service.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .factory('PositionService', ['$window', '$document', '$animate', '$timeout', '$q', PositionService]);
7 |
8 | /*
9 | * The position service
10 | *
11 | * Find the best adjustements of the elemnts in the DOM according the their
12 | * order, height and width
13 | *
14 | * Fix their absolute position in the DOM while adding a ng-animate class for
15 | * personalized animations
16 | *
17 | */
18 | function PositionService($window, $document, $animate, $timeout, $q) {
19 |
20 | // The list of ongoing animations
21 | var ongoingAnimations = {};
22 | // The list of items related to the DOM elements
23 | var items = [];
24 | // The list of the DOM elements
25 | var elements = [];
26 | // The columns that contains the items
27 | var columns = [];
28 |
29 | var self = {
30 | getItemsDimensionFromDOM: getItemsDimensionFromDOM,
31 | applyToDOM: applyToDOM,
32 | layout: layout,
33 | getColumns: getColumns
34 | };
35 | return self;
36 |
37 | /*
38 | * Get the items heights and width from the DOM
39 | * @return: the list of items with their sizes
40 | */
41 | function getItemsDimensionFromDOM() {
42 | // not(.ng-leave) : we don't want to select elements that have been
43 | // removed but are still in the DOM
44 | elements = $document[0].querySelectorAll(
45 | '.dynamic-layout-item-parent:not(.ng-leave)'
46 | );
47 | items = [];
48 | for (var i = 0; i < elements.length; ++i) {
49 | // Note: we need to get the children element width because that's
50 | // where the style is applied
51 | var rect = elements[i].children[0].getBoundingClientRect();
52 | var width;
53 | var height;
54 | if (rect.width) {
55 | width = rect.width;
56 | height = rect.height;
57 | } else {
58 | width = rect.right - rect.left;
59 | height = rect.top - rect.bottom;
60 | }
61 |
62 | items.push({
63 | height: height +
64 | parseFloat($window.getComputedStyle(elements[i]).marginTop),
65 | width: width +
66 | parseFloat(
67 | $window.getComputedStyle(elements[i].children[0]).marginLeft
68 | )
69 | });
70 | }
71 | return items;
72 | }
73 |
74 | /*
75 | * Apply positions to the DOM with an animation
76 | * @return: the promise of the position animations being completed
77 | */
78 | function applyToDOM() {
79 |
80 | var ret = $q.defer();
81 |
82 | /*
83 | * Launch an animation on a specific element
84 | * Once the animation is complete remove it from the ongoing animation
85 | * @param element: the element being moved
86 | * @param i: the index of the current animation
87 | * @return: the promise of the animation being completed
88 | */
89 | function launchAnimation(element, i) {
90 | var animationPromise = $animate.addClass(element,
91 | 'move-items-animation',
92 | {
93 | from: {
94 | position: 'absolute'
95 | },
96 | to: {
97 | left: items[i].x + 'px',
98 | top: items[i].y + 'px'
99 | }
100 | }
101 | );
102 |
103 | animationPromise.then(function() {
104 | // We remove the class so that the animation can be ran again
105 | element.classList.remove('move-items-animation');
106 | delete ongoingAnimations[i];
107 | });
108 |
109 | return animationPromise;
110 | }
111 |
112 | /*
113 | * Launch the animations on all the elements
114 | * @return: the promise of the animations being completed
115 | */
116 | function launchAnimations() {
117 | var i;
118 | for (i = 0; i < items.length; ++i) {
119 | // We need to pass the specific element we're dealing with
120 | // because at the next iteration elements[i] might point to
121 | // something else
122 | ongoingAnimations[i] = launchAnimation(elements[i], i);
123 | }
124 | $q.all(ongoingAnimations).then(function() {
125 | ret.resolve();
126 | });
127 | }
128 |
129 | // We need to cancel all ongoing animations before we start the new
130 | // ones
131 | if (Object.keys(ongoingAnimations).length) {
132 | for (var j in ongoingAnimations) {
133 | $animate.cancel(ongoingAnimations[j]);
134 | delete ongoingAnimations[j];
135 | }
136 | }
137 |
138 | // For some reason we need to launch the new animations at the next
139 | // digest
140 | $timeout(function() {
141 | launchAnimations(ret);
142 | });
143 |
144 | return ret.promise;
145 | }
146 |
147 | /*
148 | * Apply the position service on the elements in the DOM
149 | * @param containerWidth: the width of the dynamic-layout container
150 | * @return: the promise of the position animations being completed
151 | */
152 | function layout(containerWidth, colWidth) {
153 | // We first gather the items dimension based on the DOM elements
154 | items = self.getItemsDimensionFromDOM();
155 |
156 | // Then we get the column size base the elements minimum width
157 | var colWidth = colWidth || getColWidth();
158 | var nbColumns = Math.floor(containerWidth / colWidth);
159 | // We create empty columns to be filled with the items
160 | initColumns(nbColumns);
161 |
162 | // We determine what is the column size of each of the items based on
163 | // their width and the column size
164 | setItemsColumnSpan(colWidth);
165 |
166 | // We set what should be their absolute position in the DOM
167 | setItemsPosition(columns, colWidth);
168 |
169 | // We apply those positions to the DOM with an animation
170 | return self.applyToDOM();
171 | }
172 |
173 | // Make the columns public
174 | function getColumns() {
175 | return columns;
176 | }
177 |
178 | /*
179 | * Intialize the columns
180 | * @param nb: the number of columns to be initialized
181 | * @return: the empty columns
182 | */
183 | function initColumns(nb) {
184 | columns = [];
185 | var i;
186 | for (i = 0; i < nb; ++i) {
187 | columns.push([]);
188 | }
189 | return columns;
190 | }
191 |
192 | /*
193 | * Get the columns heights
194 | * @param columns: the columns with the items they contain
195 | * @return: an array of columns heights
196 | */
197 | function getColumnsHeights(cols) {
198 | var columnsHeights = [];
199 | var i;
200 | for (i in cols) {
201 | var h;
202 | if (cols[i].length) {
203 | var lastItem = cols[i][cols[i].length - 1];
204 | h = lastItem.y + lastItem.height;
205 | } else {
206 | h = 0;
207 | }
208 | columnsHeights.push(h);
209 | }
210 | return columnsHeights;
211 | }
212 |
213 | /*
214 | * Find the item absolute position and what columns it belongs too
215 | * @param item: the item to place
216 | * @param colHeights: the current heigh of the column when all items prior to this
217 | * one were places
218 | * @param colWidth: the column width
219 | * @return the item's columms and coordinates
220 | */
221 | function getItemColumnsAndPosition(item, colHeights, colWidth) {
222 | if (item.columnSpan > colHeights.length) {
223 | throw 'Item too large';
224 | }
225 |
226 | var indexOfMin = 0;
227 | var minFound = 0;
228 | var i;
229 |
230 | // We look at what set of columns have the minimum height
231 | for (i = 0; i <= colHeights.length - item.columnSpan; ++i) {
232 | var startingColumn = i;
233 | var endingColumn = i + item.columnSpan;
234 | var maxHeightInPart = Math.max.apply(
235 | Math, colHeights.slice(startingColumn, endingColumn)
236 | );
237 |
238 | if (i === 0 || maxHeightInPart < minFound) {
239 | minFound = maxHeightInPart;
240 | indexOfMin = i;
241 | }
242 | }
243 |
244 | var itemColumns = [];
245 | for (i = indexOfMin; i < indexOfMin + item.columnSpan; ++i) {
246 | itemColumns.push(i);
247 | }
248 |
249 | var position = {
250 | x: itemColumns[0] * colWidth,
251 | y: minFound
252 | };
253 |
254 | return {
255 | columns: itemColumns,
256 | position: position
257 | };
258 | }
259 |
260 | /*
261 | * Set the items' absolute position
262 | * @param columns: the empty columns
263 | * @param colWidth: the column width
264 | */
265 | function setItemsPosition(cols, colWidth) {
266 | var i;
267 | var j;
268 | for (i = 0; i < items.length; ++i) {
269 | var columnsHeights = getColumnsHeights(cols);
270 |
271 | var itemColumnsAndPosition = getItemColumnsAndPosition(items[i],
272 | columnsHeights,
273 | colWidth);
274 |
275 | // We place the item in the found columns
276 | for (j in itemColumnsAndPosition.columns) {
277 | columns[itemColumnsAndPosition.columns[j]].push(items[i]);
278 | }
279 |
280 | items[i].x = itemColumnsAndPosition.position.x;
281 | items[i].y = itemColumnsAndPosition.position.y;
282 | }
283 | }
284 |
285 | /*
286 | * Get the column size based on the minimum width of the items
287 | * @return: column size
288 | */
289 | function getColWidth() {
290 | var i;
291 | var colWidth;
292 | for (i = 0; i < items.length; ++i) {
293 | if (!colWidth || items[i].width < colWidth) {
294 | colWidth = items[i].width;
295 | }
296 | }
297 | return colWidth;
298 | }
299 |
300 | /*
301 | * Set the column span for each of the items based on their width and the
302 | * column size
303 | * @param: column size
304 | */
305 | function setItemsColumnSpan(colWidth) {
306 | var i;
307 | for (i = 0; i < items.length; ++i) {
308 | items[i].columnSpan = Math.ceil(items[i].width / colWidth);
309 | }
310 | }
311 |
312 | }
313 |
314 | })();
315 |
--------------------------------------------------------------------------------
/src/ranker.service.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | angular
5 | .module('dynamicLayout')
6 | .factory('RankerService', RankerService);
7 |
8 | /*
9 | * The rankers service
10 | *
11 | * Allows a list of rankers to sort the items.
12 | * If two items are the same regarding the first ranker, the second one is used
13 | * to part them, etc.
14 | *
15 | * Rankers can be either a property name or a custom operation on the item.
16 | * They all need to specify the order chosen (asc' or 'desc')
17 | *
18 | * var rankers = [
19 | * ['color', 'asc'],
20 | * ['atomicNumber', 'desc']
21 | * ];
22 | * Or
23 | * var rankers = [
24 | * [myCustomGetter, 'asc']
25 | * ];
26 | *
27 | */
28 | function RankerService() {
29 |
30 | return {
31 | applyRankers: applyRankers
32 | };
33 |
34 | /*
35 | * Order the items with the given rankers
36 | * @param items: the items being ranked
37 | * @param rankers: the array of rankers used to rank the items
38 | * @return the ordered list of items
39 | */
40 | function applyRankers(items, rankers) {
41 | // The ranker counter
42 | var i = 0;
43 |
44 | if (rankers) {
45 | items.sort(sorter);
46 | }
47 |
48 | /*
49 | * The custom sorting function using the built comparison function
50 | * @param a, b: the items to be compared
51 | * @return -1, 0 or 1
52 | */
53 | function sorter(a, b) {
54 | i = 0;
55 | return recursiveRanker(a, b);
56 | }
57 |
58 | /*
59 | * Compare recursively two items
60 | * It first compare the items with the first ranker, if no conclusion
61 | * can be drawn it uses the second ranker and so on until it finds a
62 | * winner or there are no more rankers
63 | * @param a, b: the items to be compared
64 | * @return -1, 0 or 1
65 | */
66 | function recursiveRanker(a, b) {
67 | var ranker = rankers[i][0];
68 | var ascDesc = rankers[i][1];
69 | var valueA;
70 | var valueB;
71 | // If it is a custom ranker, give the item as input and gather the
72 | // ouput
73 | if (angular.isFunction(ranker)) {
74 | valueA = ranker(a);
75 | valueB = ranker(b);
76 | } else {
77 | // Otherwise use the item's properties
78 | if (!(ranker in a) && !(ranker in b)) {
79 | valueA = 0;
80 | valueB = 0;
81 | } else if (!(ranker in a)) {
82 | return ascDesc === 'asc' ? -1 : 1;
83 | } else if (!(ranker in b)) {
84 | return ascDesc === 'asc' ? 1 : -1;
85 | }
86 | valueA = a[ranker];
87 | valueB = b[ranker];
88 | }
89 |
90 | if (typeof valueA === typeof valueB) {
91 |
92 | if (angular.isString(valueA)) {
93 | var comp = valueA.localeCompare(valueB);
94 | if (comp === 1) {
95 | return ascDesc === 'asc' ? 1 : -1;
96 | } else if (comp === -1) {
97 | return ascDesc === 'asc' ? -1 : 1;
98 | }
99 | } else {
100 | if (valueA > valueB) {
101 | return ascDesc === 'asc' ? 1 : -1;
102 | } else if (valueA < valueB) {
103 | return ascDesc === 'asc' ? -1 : 1;
104 | }
105 | }
106 | }
107 |
108 | ++i;
109 |
110 | if (rankers.length > i) {
111 | return recursiveRanker(a, b);
112 | }
113 |
114 | return 0;
115 | }
116 |
117 | return items;
118 | }
119 |
120 | }
121 |
122 | })();
123 |
--------------------------------------------------------------------------------
/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "jasmine": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tests/filter.spec.js:
--------------------------------------------------------------------------------
1 | /* globals inject */
2 | (function() {
3 | 'use strict';
4 |
5 | describe('FilterService', function() {
6 |
7 | beforeEach(module('dynamicLayout'));
8 |
9 | it('check that apply function exists', inject(function(FilterService) {
10 | expect( FilterService.applyFilters ).toBeDefined();
11 | }));
12 |
13 | it('check that filter service work properly',
14 | inject(function(FilterService) {
15 |
16 | var mockedItems = [
17 | {
18 | id: 1,
19 | color: 'red',
20 | atomicNumber: 45.65
21 | },
22 | {
23 | id: 2,
24 | color: 'green',
25 | atomicNumber: 4.2
26 | },
27 | {
28 | id: 3,
29 | color: 'black',
30 | atomicNumber: 4
31 | },
32 | {
33 | id: 4,
34 | color: 'grey',
35 | atomicNumber: 60
36 | },
37 | {
38 | id: 5,
39 | color: 'grey',
40 | atomicNumber: 1.8
41 | }
42 | ];
43 |
44 | var filters = [ // an AND goup compose of OR groups
45 | [ // an OR group compose of statements
46 | ['color', '=', 'grey'], // A statement
47 | ['color', '=', 'black']
48 | ],
49 | [ // a second OR goup composed of statements
50 | ['atomicNumber', '<', 3]
51 | ]
52 | ];
53 |
54 | var itemsRes = FilterService.applyFilters(mockedItems, filters);
55 |
56 | expect(itemsRes.length).toEqual(1);
57 | expect(itemsRes[0].id ).toEqual(5);
58 |
59 | var myCustomFilter = function(item) {
60 | if (item.color !== 'red') {
61 | return true;
62 | }
63 | return false;
64 | };
65 |
66 | filters = [
67 | [myCustomFilter]
68 | ];
69 |
70 | itemsRes = FilterService.applyFilters(mockedItems, filters);
71 | expect(itemsRes.length).toEqual(4);
72 | })
73 | );
74 |
75 | it('check that the filter service reject invalid comparators',
76 | inject(function(FilterService) {
77 |
78 | var mockedItems = [
79 | {
80 | id: 1,
81 | color: 'red',
82 | atomicNumber: 45.65
83 | }
84 | ];
85 |
86 | var filters = [[
87 | ['atomicNumber', 'invalid']
88 | ]];
89 |
90 | expect(function() {
91 | FilterService.applyFilters(mockedItems, filters);
92 | }).toThrow('Incorrect statement');
93 |
94 | filters = [[
95 | ['atomicNumber', 'invalid', 3]
96 | ]];
97 |
98 | expect(function() {
99 | FilterService.applyFilters(mockedItems, filters);
100 | }).toThrow('Incorrect statement comparator: invalid');
101 |
102 | })
103 | );
104 |
105 | it('check that the filter service reject invalid property for\
106 | "contains" comparator',
107 | inject(function(FilterService) {
108 | var mockedItems = [
109 | {
110 | id: 1,
111 | color: 'red',
112 | atomicNumber: 45.65
113 | }
114 | ];
115 |
116 | var filters = [[
117 | ['atomicNumber', 'contains', 45]
118 | ]];
119 |
120 | expect(function() {
121 | FilterService.applyFilters(mockedItems, filters);
122 | }).toThrow('contains statement has to be applied on array');
123 | })
124 | );
125 |
126 | });
127 |
128 | })();
129 |
--------------------------------------------------------------------------------
/tests/position.spec.js:
--------------------------------------------------------------------------------
1 | /* globals inject */
2 | (function() {
3 | 'use strict';
4 |
5 | describe('PositionService', function() {
6 |
7 | beforeEach(module('dynamicLayout'));
8 |
9 | it('check that apply function exists', inject(function(PositionService) {
10 | expect(PositionService.layout).toBeDefined();
11 | }));
12 |
13 | it('check that positions work properly',
14 | inject(function($q, PositionService) {
15 |
16 | var items = [
17 | {
18 | id: 1,
19 | color: 'red',
20 | atomicNumber: 45.65,
21 | height: 10,
22 | width: 100
23 | },
24 | {
25 | id: 2,
26 | color: 'green',
27 | atomicNumber: 4.2,
28 | height: 20,
29 | width: 100
30 | },
31 | {
32 | id: 3,
33 | color: 'black',
34 | atomicNumber: 4,
35 | height: 150,
36 | width: 100
37 | },
38 | {
39 | id: 4,
40 | color: 'grey',
41 | atomicNumber: 60,
42 | height: 60,
43 | width: 200
44 | },
45 | {
46 | id: 5,
47 | color: 'grey',
48 | atomicNumber: 1.8,
49 | height: 30,
50 | width: 100
51 | }
52 | ];
53 |
54 | // Disable DOM manipulation
55 | spyOn(PositionService, 'getItemsDimensionFromDOM')
56 | .and.returnValue(items);
57 | spyOn(PositionService, 'applyToDOM')
58 | .and.returnValue($q.defer().promise);
59 |
60 | // Test that items were properly set up in the grid
61 | // Input: list of items with their dimensions (width, height)
62 | // Output: x,y of each item
63 |
64 | var promise = PositionService.layout(300);
65 | expect(promise.then).toBeDefined();
66 |
67 | var columns = PositionService.getColumns();
68 |
69 | expect(columns[0].length).toEqual(3);
70 | expect(columns[1].length).toEqual(2);
71 | expect(columns[2].length).toEqual(1);
72 |
73 | expect(columns[0][0].id).toEqual(1);
74 | expect(columns[0][1].id).toEqual(4);
75 | expect(columns[0][2].id).toEqual(5);
76 |
77 | expect(columns[1][0].id).toEqual(2);
78 | expect(columns[1][1].id).toEqual(4);
79 |
80 | expect(columns[2][0].id).toEqual(3);
81 |
82 | }));
83 |
84 | it('check that item too large is detected and throws errors',
85 | inject(function(PositionService) {
86 | var items = [
87 | {
88 | id: 1,
89 | color: 'red',
90 | atomicNumber: 45.65,
91 | height: 10,
92 | width: 600
93 | }
94 | ];
95 |
96 | spyOn(PositionService, 'getItemsDimensionFromDOM')
97 | .and.returnValue(items);
98 | spyOn(PositionService, 'applyToDOM');
99 |
100 | expect(function() {
101 | PositionService.layout(300);
102 | }).toThrow('Item too large');
103 |
104 | })
105 | );
106 |
107 | });
108 |
109 | })();
110 |
--------------------------------------------------------------------------------
/tests/ranker.spec.js:
--------------------------------------------------------------------------------
1 | /* globals inject */
2 | (function() {
3 | 'use strict';
4 |
5 | describe('RankerService', function() {
6 |
7 | beforeEach(module('dynamicLayout'));
8 |
9 | it('check that apply function exists', inject(function(RankerService) {
10 | expect(RankerService.applyRankers).toBeDefined();
11 | }));
12 |
13 | it('check that rankers work properly',
14 | inject(function(RankerService) {
15 |
16 | var items = [
17 | {
18 | id: 1,
19 | color: 'red',
20 | atomicNumber: 45.65
21 | },
22 | {
23 | id: 2,
24 | color: 'green',
25 | atomicNumber: 4.2
26 | },
27 | {
28 | id: 3,
29 | color: 'black',
30 | atomicNumber: 4
31 | },
32 | {
33 | id: 4,
34 | color: 'grey',
35 | atomicNumber: 60
36 | },
37 | {
38 | id: 5,
39 | color: 'grey',
40 | atomicNumber: 1.8
41 | }
42 | ];
43 |
44 | var rankers = [
45 | ['color', 'asc'],
46 | ['atomicNumber', 'desc']
47 | ];
48 |
49 | var itemsRes = RankerService.applyRankers(angular.copy(items), rankers);
50 |
51 | expect(itemsRes[0].id).toEqual(3);
52 | expect(itemsRes[1].id).toEqual(2);
53 | expect(itemsRes[2].id).toEqual(4);
54 | expect(itemsRes[3].id).toEqual(5);
55 | expect(itemsRes[4].id).toEqual(1);
56 |
57 | var myCustomGetter = function(item) {
58 | if (item.atomicNumber > 5) {
59 | return 1;
60 | }
61 | return 0;
62 | };
63 |
64 | rankers = [
65 | [myCustomGetter, 'asc']
66 | ];
67 | itemsRes = RankerService.applyRankers(angular.copy(items), rankers);
68 | expect(itemsRes[0].id).toEqual(2);
69 | expect(itemsRes[1].id).toEqual(3);
70 | expect(itemsRes[2].id).toEqual(5);
71 | expect(itemsRes[3].id).toEqual(1);
72 | expect(itemsRes[4].id).toEqual(4);
73 |
74 | }));
75 |
76 | });
77 |
78 | })();
79 |
--------------------------------------------------------------------------------