diff --git a/.gitignore b/.gitignore index bb93d68..8dd3dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -bower_components \ No newline at end of file +bower_components +.idea +*.iml \ No newline at end of file diff --git a/README.md b/README.md index 9b819d1..74caa13 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,35 @@ This index reflects the index of the corresponding object in the source collecti That's all! Ehm, no. If you run your application now, you will notice that there is only one column. What is missing? Well, we have to define the configuration for the visual representation. And what is the best place for something like this? Yes, for sure! Your CSS file(s). +### Providing a custom function to build the watch listener + +If you are expecting a lot of items or have complex objects you may want to provide a custom function to build a watch expression instead of asking angular to deep compare each object. The function should return a string and will be called for each item in `source`. The result will be a concatenated string from the result of each function. + +Example: + +```html +
+ +
+``` + +```javascript +$scope.items = [ + { id: 1, name: "Photo 1", properties: {...} }, + { id: 2, name: "Photo 2", properties: {...} }, + { id: 3, name: "Photo 3", properties: {...} }, + { id: 4, name: "Photo 4", properties: {...} }, +]; + +$scope.getItemId = function (item) { + return item.id + "|"; +} +``` + +The value used for the watch expression will be: `'1|2|3|4|'`. + ## The grid configuration The grid items will be distributed by your configured CSS selectors. An example: diff --git a/angular-deckgrid.js b/angular-deckgrid.js index fe7ab1f..75215c2 100644 --- a/angular-deckgrid.js +++ b/angular-deckgrid.js @@ -100,8 +100,8 @@ angular.module('akoenig.deckgrid').factory('DeckgridDescriptor', [ scope.$on('$destroy', this.$$destroy.bind(this)); - if (attrs.cardtemplate === undefined) { - if (attrs.cardtemplatestring === undefined) { + if (angular.isUndefined(attrs.cardtemplate)) { + if (angular.isUndefined(attrs.cardtemplatestring)) { // use the provided inner html as template transclude(scope, function onTransclude (innerHTML) { var extractedInnerHTML = [], @@ -112,7 +112,7 @@ angular.module('akoenig.deckgrid').factory('DeckgridDescriptor', [ for (i; i < len; i = i + 1) { outerHTML = innerHTML[i].outerHTML; - if (outerHTML !== undefined) { + if (angular.isDefined(outerHTML)) { extractedInnerHTML.push(outerHTML); } } @@ -161,254 +161,281 @@ angular.module('akoenig.deckgrid').factory('DeckgridDescriptor', [ angular.module('akoenig.deckgrid').factory('Deckgrid', [ - '$window', - '$log', - - function initialize ($window, $log) { - - 'use strict'; - - /** - * The deckgrid directive. - * - */ - function Deckgrid (scope, element) { - var self = this, - watcher, - mql; - - this.$$elem = element; - this.$$watchers = []; - - this.$$scope = scope; - this.$$scope.columns = []; - - // - // The layout configuration will be parsed from - // the pseudo "before element." There you have to save all - // the column configurations. - // - this.$$scope.layout = this.$$getLayout(); - - this.$$createColumns(); - - // - // Register model change. - // - watcher = this.$$scope.$watch('model', this.$$onModelChange.bind(this), true); - this.$$watchers.push(watcher); - - // - // Register media query change events. - // - angular.forEach(self.$$getMediaQueries(), function onIteration (rule) { - var handler = self.$$onMediaQueryChange.bind(self); - - function onDestroy () { - rule.removeListener(handler); - } - - rule.addListener(handler); - - self.$$watchers.push(onDestroy); - }); - - mql = $window.matchMedia('(orientation: portrait)'); - mql.addListener(self.$$onMediaQueryChange.bind(self)); - - } - - /** - * @private - * - * Extracts the media queries out of the stylesheets. - * - * This method will fetch the media queries out of the stylesheets that are - * responsible for styling the angular-deckgrid. - * - * @return {array} An array with all respective styles. - * - */ - Deckgrid.prototype.$$getMediaQueries = function $$getMediaQueries () { - var stylesheets = [], - mediaQueries = []; - - stylesheets = Array.prototype.concat.call( - Array.prototype.slice.call(document.querySelectorAll('style[type=\'text/css\']')), - Array.prototype.slice.call(document.querySelectorAll('link[rel=\'stylesheet\']')) - ); - - function extractRules (stylesheet) { - try { - return (stylesheet.sheet.cssRules || []); - } catch (e) { - return []; - } - } - - function hasDeckgridStyles (rule) { - var regexe = /\[(\w*-)?deckgrid\]::?before/g, - i = 0, - selector = ''; - - if (!rule.media || angular.isUndefined(rule.cssRules)) { - return false; - } - - i = rule.cssRules.length - 1; - - for (i; i >= 0; i = i - 1) { - selector = rule.cssRules[i].selectorText; - - if (angular.isDefined(selector) && selector.match(regexe)) { - return true; - } - } - - return false; - } - - angular.forEach(stylesheets, function onIteration (stylesheet) { - var rules = extractRules(stylesheet); - - angular.forEach(rules, function inRuleIteration (rule) { - if (hasDeckgridStyles(rule)) { - mediaQueries.push($window.matchMedia(rule.media.mediaText)); - } - }); - }); - - return mediaQueries; - }; - - /** - * @private - * - * Creates the column segmentation. With other words: - * This method creates the internal data structure from the - * passed "source" attribute. Every card within this "source" - * model will be passed into this internal column structure by - * reference. So if you modify the data within your controller - * this directive will reflect these changes immediately. - * - * NOTE that calling this method will trigger a complete template "redraw". - * - */ - Deckgrid.prototype.$$createColumns = function $$createColumns () { - var self = this; - - if (!this.$$scope.layout) { - return $log.error('angular-deckgrid: No CSS configuration found (see ' + - 'https://github.com/akoenig/angular-deckgrid#the-grid-configuration)'); - } - - this.$$scope.columns = []; - - angular.forEach(this.$$scope.model, function onIteration (card, index) { - var column = (index % self.$$scope.layout.columns) | 0; - - if (!self.$$scope.columns[column]) { - self.$$scope.columns[column] = []; - } - - card.$index = index; - self.$$scope.columns[column].push(card); - }); - }; - - /** - * @private - * - * Parses the configuration out of the configured CSS styles. - * - * Example: - * - * .deckgrid::before { + '$window', + '$log', + + function initialize($window, $log) { + + 'use strict'; + + /** + * The deckgrid directive. + * + */ + function Deckgrid(scope, element) { + var self = this, + watcher, + mql; + + this.$$elem = element; + this.$$watchers = []; + + this.$$scope = scope; + this.$$scope.columns = []; + + // + // The layout configuration will be parsed from + // the pseudo "before element." There you have to save all + // the column configurations. + // + this.$$scope.layout = this.$$getLayout(); + + this.$$createColumns(); + + // + // Register model change. + // + watcher = this.$$scope.$watchCollection('model', this.$$onModelChange.bind(this)); + this.$$watchers.push(watcher); + + // + // Register media query change events. + // + angular.forEach(self.$$getMediaQueries(), function onIteration(rule) { + var handler = self.$$onMediaQueryChange.bind(self); + + function onDestroy() { + rule.removeListener(handler); + } + + rule.addListener(handler); + + self.$$watchers.push(onDestroy); + }); + + mql = $window.matchMedia('(orientation: portrait)'); + mql.addListener(self.$$onMediaQueryChange.bind(self)); + + } + + /** + * @private + * + * Extracts the media queries out of the stylesheets. + * + * This method will fetch the media queries out of the stylesheets that are + * responsible for styling the angular-deckgrid. + * + * @return {array} An array with all respective styles. + * + */ + Deckgrid.prototype.$$getMediaQueries = function $$getMediaQueries() { + var stylesheets = [], + mediaQueries = []; + + stylesheets = Array.prototype.concat.call( + Array.prototype.slice.call(document.querySelectorAll('style[type=\'text/css\']')), + Array.prototype.slice.call(document.querySelectorAll('link[rel=\'stylesheet\']')) + ); + + function extractRules(stylesheet) { + try { + return (stylesheet.sheet.cssRules || []); + } catch (e) { + return []; + } + } + + function hasDeckgridStyles(rule) { + var regexe = /\[(\w*-)?deckgrid\]::?before/g, + i = 0, + selector = ''; + + if (!rule.media || angular.isUndefined(rule.cssRules)) { + return false; + } + + i = rule.cssRules.length - 1; + + for (i; i >= 0; i = i - 1) { + selector = rule.cssRules[i].selectorText; + + if (angular.isDefined(selector) && selector.match(regexe)) { + return true; + } + } + + return false; + } + + angular.forEach(stylesheets, function onIteration(stylesheet) { + var rules = extractRules(stylesheet); + + angular.forEach(rules, function inRuleIteration(rule) { + if (hasDeckgridStyles(rule)) { + mediaQueries.push($window.matchMedia(rule.media.mediaText)); + } + }); + }); + + return mediaQueries; + }; + + Deckgrid.prototype.$$cachedParams = function $$cachedParams(column) { + if (!this.$$cache) { + this.$$cache = {}; + } + if (this.$$cache.column === column) { + return this.$$cache; + } + this.$$cache = { column: column, modelLength: 0 }; + return this.$$cache; + }; + + /** + * @private + * + * Creates the column segmentation. With other words: + * This method creates the internal data structure from the + * passed "source" attribute. Every card within this "source" + * model will be passed into this internal column structure by + * reference. So if you modify the data within your controller + * this directive will reflect these changes immediately. + * + * NOTE that calling this method will trigger a complete template "redraw". + * + */ + Deckgrid.prototype.$$createColumns = function $$createColumns() { + if (!this.$$scope.layout) { + return $log.error('angular-deckgrid: No CSS configuration found (see ' + + 'https://github.com/akoenig/angular-deckgrid#the-grid-configuration)'); + } + + var cachedParams = this.$$cachedParams(this.$$scope.layout.columns); + if (cachedParams.modelLength === 0) { + // layout change, reset columns + this.$$scope.columns = []; + } + + var modelLength = 0; + if (this.$$scope.model) { + modelLength = this.$$scope.model.length; + } + for (var index = cachedParams.modelLength; index < modelLength; index++) { + var card = this.$$scope.model[index]; + var column = (index % this.$$scope.layout.columns) | 0; + + if (!this.$$scope.columns[column]) { + this.$$scope.columns[column] = []; + } + card.$index = index; + this.$$scope.columns[column].push(card); + } + + cachedParams.modelLength = modelLength; + }; + + /** + * @private + * + * Parses the configuration out of the configured CSS styles. + * + * Example: + * + * .deckgrid::before { * content: '3 .column.size-1-3'; * } - * - * Will result in a three column grid where each column will have the - * classes: "column size-1-3". - * - * You are responsible for defining the respective styles within your CSS. - * - */ - Deckgrid.prototype.$$getLayout = function $$getLayout () { - var content = $window.getComputedStyle(this.$$elem, ':before').content, - layout; - - if (content) { - content = content.replace(/'/g, ''); // before e.g. '3 .column.size-1of3' - content = content.replace(/"/g, ''); // before e.g. "3 .column.size-1of3" - content = content.split(' '); - - if (2 === content.length) { - layout = {}; - layout.columns = (content[0] | 0); - layout.classList = content[1].replace(/\./g, ' ').trim(); - } - } - - return layout; - }; - - /** - * @private - * - * Event that will be triggered if a CSS media query changed. - * - */ - Deckgrid.prototype.$$onMediaQueryChange = function $$onMediaQueryChange () { - var self = this, - layout = this.$$getLayout(); - - // - // Okay, the layout has changed. - // Creating a new column structure is not avoidable. - // - if (layout.columns !== this.$$scope.layout.columns) { - self.$$scope.layout = layout; - - self.$$scope.$apply(function onApply () { - self.$$createColumns(); - }); - } - }; - - /** - * @private - * - * Event that will be triggered when the source model has changed. - * - */ - Deckgrid.prototype.$$onModelChange = function $$onModelChange (newModel, oldModel) { - var self = this; - - newModel = newModel || []; - oldModel = oldModel || []; - - if (oldModel.length !== newModel.length) { - self.$$createColumns(); - } - }; - - /** - * Destroys the directive. Takes care of cleaning all - * watchers and event handlers. - * - */ - Deckgrid.prototype.destroy = function destroy () { - var i = this.$$watchers.length - 1; - - for (i; i >= 0; i = i - 1) { - this.$$watchers[i](); - } - }; - - return { - create : function create (scope, element) { - return new Deckgrid(scope, element); - } - }; - } + * + * Will result in a three column grid where each column will have the + * classes: "column size-1-3". + * + * You are responsible for defining the respective styles within your CSS. + * + */ + Deckgrid.prototype.$$getLayout = function $$getLayout() { + var content = $window.getComputedStyle(this.$$elem, ':before').content, + layout; + + if (content) { + content = content.replace(/'/g, ''); // before e.g. '3 .column.size-1of3' + content = content.replace(/"/g, ''); // before e.g. "3 .column.size-1of3" + content = content.split(' '); + + if (2 === content.length) { + layout = {}; + layout.columns = (content[0] | 0); + layout.classList = content[1].replace(/\./g, ' ').trim(); + } + } + + return layout; + }; + + /** + * @private + * + * Event that will be triggered if a CSS media query changed. + * + */ + Deckgrid.prototype.$$onMediaQueryChange = function $$onMediaQueryChange() { + var self = this, + layout = this.$$getLayout(); + + // + // Okay, the layout has changed. + // Creating a new column structure is not avoidable. + // + if (layout.columns !== this.$$scope.layout.columns) { + self.$$scope.layout = layout; + + self.$$scope.$apply(function onApply() { + self.$$createColumns(); + }); + } + }; + + /** + * @private + * + * Event that will be triggered when the source model has changed. + * + */ + Deckgrid.prototype.$$onModelChange = function $$onModelChange(newModel, oldModel) { + var self = this; + + newModel = newModel || []; + oldModel = oldModel || []; + + if (oldModel.length !== newModel.length) { + self.$$createColumns(); + } else { + var i = newModel.length - 1; + for (i; i >= 0; i = i - 1) { + if (oldModel[i] !== newModel[i]) { + self.$$createColumns(); + break; + } + } + } + }; + + /** + * Destroys the directive. Takes care of cleaning all + * watchers and event handlers. + * + */ + Deckgrid.prototype.destroy = function destroy() { + var i = this.$$watchers.length - 1; + + for (i; i >= 0; i = i - 1) { + this.$$watchers[i](); + } + }; + + return { + create: function create(scope, element) { + return new Deckgrid(scope, element); + } + }; + } ]); diff --git a/angular-deckgrid.min.js b/angular-deckgrid.min.js index 35de844..7744bb2 100644 --- a/angular-deckgrid.min.js +++ b/angular-deckgrid.min.js @@ -1,2 +1,2 @@ /*! angular-deckgrid (v0.4.4) - Copyright: 2013 - 2014, André König (andre.koenig@posteo.de) - MIT */ -angular.module("akoenig.deckgrid",[]),angular.module("akoenig.deckgrid").directive("deckgrid",["DeckgridDescriptor",function(a){"use strict";return a.create()}]),angular.module("akoenig.deckgrid").factory("DeckgridDescriptor",["Deckgrid","$templateCache",function(a,b){"use strict";function c(){this.restrict="AE",this.template='
',this.scope={model:"=source"},this.$$deckgrid=null,this.transclude=!0,this.link=this.$$link.bind(this),this.$$templateKeyIndex=0}return c.prototype.$$destroy=function(){this.$$deckgrid.destroy()},c.prototype.$$link=function(c,d,e,f,g){var h="deckgrid/innerHtmlTemplate"+ ++this.$$templateKeyIndex+".html";c.$on("$destroy",this.$$destroy.bind(this)),void 0===e.cardtemplate?(void 0===e.cardtemplatestring?g(c,function(a){var c,d=[],e=0,f=a.length;for(e;f>e;e+=1)c=a[e].outerHTML,void 0!==c&&d.push(c);b.put(h,d.join())}):b.put(h,d.attr("cardtemplatestring")),c.cardTemplate=h):c.cardTemplate=e.cardtemplate,c.mother=c.$parent,this.$$deckgrid=a.create(c,d[0])},{create:function(){return new c}}}]),angular.module("akoenig.deckgrid").factory("Deckgrid",["$window","$log",function(a,b){"use strict";function c(b,c){var d,e,f=this;this.$$elem=c,this.$$watchers=[],this.$$scope=b,this.$$scope.columns=[],this.$$scope.layout=this.$$getLayout(),this.$$createColumns(),d=this.$$scope.$watch("model",this.$$onModelChange.bind(this),!0),this.$$watchers.push(d),angular.forEach(f.$$getMediaQueries(),function(a){function b(){a.removeListener(c)}var c=f.$$onMediaQueryChange.bind(f);a.addListener(c),f.$$watchers.push(b)}),e=a.matchMedia("(orientation: portrait)"),e.addListener(f.$$onMediaQueryChange.bind(f))}return c.prototype.$$getMediaQueries=function(){function b(a){try{return a.sheet.cssRules||[]}catch(b){return[]}}function c(a){var b=/\[(\w*-)?deckgrid\]::?before/g,c=0,d="";if(!a.media||angular.isUndefined(a.cssRules))return!1;for(c=a.cssRules.length-1;c>=0;c-=1)if(d=a.cssRules[c].selectorText,angular.isDefined(d)&&d.match(b))return!0;return!1}var d=[],e=[];return d=Array.prototype.concat.call(Array.prototype.slice.call(document.querySelectorAll("style[type='text/css']")),Array.prototype.slice.call(document.querySelectorAll("link[rel='stylesheet']"))),angular.forEach(d,function(d){var f=b(d);angular.forEach(f,function(b){c(b)&&e.push(a.matchMedia(b.media.mediaText))})}),e},c.prototype.$$createColumns=function(){var a=this;return this.$$scope.layout?(this.$$scope.columns=[],void angular.forEach(this.$$scope.model,function(b,c){var d=c%a.$$scope.layout.columns|0;a.$$scope.columns[d]||(a.$$scope.columns[d]=[]),b.$index=c,a.$$scope.columns[d].push(b)})):b.error("angular-deckgrid: No CSS configuration found (see https://github.com/akoenig/angular-deckgrid#the-grid-configuration)")},c.prototype.$$getLayout=function(){var b,c=a.getComputedStyle(this.$$elem,":before").content;return c&&(c=c.replace(/'/g,""),c=c.replace(/"/g,""),c=c.split(" "),2===c.length&&(b={},b.columns=0|c[0],b.classList=c[1].replace(/\./g," ").trim())),b},c.prototype.$$onMediaQueryChange=function(){var a=this,b=this.$$getLayout();b.columns!==this.$$scope.layout.columns&&(a.$$scope.layout=b,a.$$scope.$apply(function(){a.$$createColumns()}))},c.prototype.$$onModelChange=function(a,b){var c=this;a=a||[],b=b||[],b.length!==a.length&&c.$$createColumns()},c.prototype.destroy=function(){var a=this.$$watchers.length-1;for(a;a>=0;a-=1)this.$$watchers[a]()},{create:function(a,b){return new c(a,b)}}}]); \ No newline at end of file +angular.module("akoenig.deckgrid",[]),angular.module("akoenig.deckgrid").directive("deckgrid",["DeckgridDescriptor",function(a){"use strict";return a.create()}]),angular.module("akoenig.deckgrid").factory("DeckgridDescriptor",["Deckgrid","$templateCache",function(a,b){"use strict";function c(){this.restrict="AE",this.template='
',this.scope={model:"=source"},this.$$deckgrid=null,this.transclude=!0,this.link=this.$$link.bind(this),this.$$templateKeyIndex=0}return c.prototype.$$destroy=function(){this.$$deckgrid.destroy()},c.prototype.$$link=function(c,d,e,f,g){var h="deckgrid/innerHtmlTemplate"+ ++this.$$templateKeyIndex+".html";c.$on("$destroy",this.$$destroy.bind(this)),angular.isUndefined(e.cardtemplate)?(angular.isUndefined(e.cardtemplatestring)?g(c,function(a){var c,d=[],e=0,f=a.length;for(e;f>e;e+=1)c=a[e].outerHTML,angular.isDefined(c)&&d.push(c);b.put(h,d.join())}):b.put(h,d.attr("cardtemplatestring")),c.cardTemplate=h):c.cardTemplate=e.cardtemplate,c.mother=c.$parent,this.$$deckgrid=a.create(c,d[0])},{create:function(){return new c}}}]),angular.module("akoenig.deckgrid").factory("Deckgrid",["$window","$log",function(a,b){"use strict";function c(b,c){var d,e,f=this;this.$$elem=c,this.$$watchers=[],this.$$scope=b,this.$$scope.columns=[],this.$$scope.layout=this.$$getLayout(),this.$$createColumns(),d=this.$$scope.$watchCollection("model",this.$$onModelChange.bind(this)),this.$$watchers.push(d),angular.forEach(f.$$getMediaQueries(),function(a){function b(){a.removeListener(c)}var c=f.$$onMediaQueryChange.bind(f);a.addListener(c),f.$$watchers.push(b)}),e=a.matchMedia("(orientation: portrait)"),e.addListener(f.$$onMediaQueryChange.bind(f))}return c.prototype.$$getMediaQueries=function(){function b(a){try{return a.sheet.cssRules||[]}catch(b){return[]}}function c(a){var b=/\[(\w*-)?deckgrid\]::?before/g,c=0,d="";if(!a.media||angular.isUndefined(a.cssRules))return!1;for(c=a.cssRules.length-1;c>=0;c-=1)if(d=a.cssRules[c].selectorText,angular.isDefined(d)&&d.match(b))return!0;return!1}var d=[],e=[];return d=Array.prototype.concat.call(Array.prototype.slice.call(document.querySelectorAll("style[type='text/css']")),Array.prototype.slice.call(document.querySelectorAll("link[rel='stylesheet']"))),angular.forEach(d,function(d){var f=b(d);angular.forEach(f,function(b){c(b)&&e.push(a.matchMedia(b.media.mediaText))})}),e},c.prototype.$$cachedParams=function(a){return this.$$cache||(this.$$cache={}),this.$$cache.column===a?this.$$cache:(this.$$cache={column:a,modelLength:0},this.$$cache)},c.prototype.$$createColumns=function(){if(!this.$$scope.layout)return b.error("angular-deckgrid: No CSS configuration found (see https://github.com/akoenig/angular-deckgrid#the-grid-configuration)");var a=this.$$cachedParams(this.$$scope.layout.columns);0===a.modelLength&&(this.$$scope.columns=[]);var c=0;this.$$scope.model&&(c=this.$$scope.model.length);for(var d=a.modelLength;c>d;d++){var e=this.$$scope.model[d],f=d%this.$$scope.layout.columns|0;this.$$scope.columns[f]||(this.$$scope.columns[f]=[]),e.$index=d,this.$$scope.columns[f].push(e)}a.modelLength=c},c.prototype.$$getLayout=function(){var b,c=a.getComputedStyle(this.$$elem,":before").content;return c&&(c=c.replace(/'/g,""),c=c.replace(/"/g,""),c=c.split(" "),2===c.length&&(b={},b.columns=0|c[0],b.classList=c[1].replace(/\./g," ").trim())),b},c.prototype.$$onMediaQueryChange=function(){var a=this,b=this.$$getLayout();b.columns!==this.$$scope.layout.columns&&(a.$$scope.layout=b,a.$$scope.$apply(function(){a.$$createColumns()}))},c.prototype.$$onModelChange=function(a,b){var c=this;if(a=a||[],b=b||[],b.length!==a.length)c.$$createColumns();else{var d=a.length-1;for(d;d>=0;d-=1)if(b[d]!==a[d]){c.$$createColumns();break}}},c.prototype.destroy=function(){var a=this.$$watchers.length-1;for(a;a>=0;a-=1)this.$$watchers[a]()},{create:function(a,b){return new c(a,b)}}}]); \ No newline at end of file diff --git a/bower.json b/bower.json index c2cdd46..4766802 100644 --- a/bower.json +++ b/bower.json @@ -15,7 +15,6 @@ "bower_components", "src", "test", - "*.min.js", "*.conf.js", "*.html", "Gruntfile.js", diff --git a/src/deckgrid.js b/src/deckgrid.js index cb07dd7..52bdebe 100644 --- a/src/deckgrid.js +++ b/src/deckgrid.js @@ -13,254 +13,281 @@ angular.module('akoenig.deckgrid').factory('Deckgrid', [ - '$window', - '$log', - - function initialize ($window, $log) { - - 'use strict'; - - /** - * The deckgrid directive. - * - */ - function Deckgrid (scope, element) { - var self = this, - watcher, - mql; - - this.$$elem = element; - this.$$watchers = []; - - this.$$scope = scope; - this.$$scope.columns = []; - - // - // The layout configuration will be parsed from - // the pseudo "before element." There you have to save all - // the column configurations. - // - this.$$scope.layout = this.$$getLayout(); - - this.$$createColumns(); - - // - // Register model change. - // - watcher = this.$$scope.$watch('model', this.$$onModelChange.bind(this), true); - this.$$watchers.push(watcher); - - // - // Register media query change events. - // - angular.forEach(self.$$getMediaQueries(), function onIteration (rule) { - var handler = self.$$onMediaQueryChange.bind(self); - - function onDestroy () { - rule.removeListener(handler); - } - - rule.addListener(handler); - - self.$$watchers.push(onDestroy); - }); - - mql = $window.matchMedia('(orientation: portrait)'); - mql.addListener(self.$$onMediaQueryChange.bind(self)); - - } - - /** - * @private - * - * Extracts the media queries out of the stylesheets. - * - * This method will fetch the media queries out of the stylesheets that are - * responsible for styling the angular-deckgrid. - * - * @return {array} An array with all respective styles. - * - */ - Deckgrid.prototype.$$getMediaQueries = function $$getMediaQueries () { - var stylesheets = [], - mediaQueries = []; - - stylesheets = Array.prototype.concat.call( - Array.prototype.slice.call(document.querySelectorAll('style[type=\'text/css\']')), - Array.prototype.slice.call(document.querySelectorAll('link[rel=\'stylesheet\']')) - ); - - function extractRules (stylesheet) { - try { - return (stylesheet.sheet.cssRules || []); - } catch (e) { - return []; - } - } - - function hasDeckgridStyles (rule) { - var regexe = /\[(\w*-)?deckgrid\]::?before/g, - i = 0, - selector = ''; - - if (!rule.media || angular.isUndefined(rule.cssRules)) { - return false; - } - - i = rule.cssRules.length - 1; - - for (i; i >= 0; i = i - 1) { - selector = rule.cssRules[i].selectorText; - - if (angular.isDefined(selector) && selector.match(regexe)) { - return true; - } - } - - return false; - } - - angular.forEach(stylesheets, function onIteration (stylesheet) { - var rules = extractRules(stylesheet); - - angular.forEach(rules, function inRuleIteration (rule) { - if (hasDeckgridStyles(rule)) { - mediaQueries.push($window.matchMedia(rule.media.mediaText)); - } - }); - }); - - return mediaQueries; - }; - - /** - * @private - * - * Creates the column segmentation. With other words: - * This method creates the internal data structure from the - * passed "source" attribute. Every card within this "source" - * model will be passed into this internal column structure by - * reference. So if you modify the data within your controller - * this directive will reflect these changes immediately. - * - * NOTE that calling this method will trigger a complete template "redraw". - * - */ - Deckgrid.prototype.$$createColumns = function $$createColumns () { - var self = this; - - if (!this.$$scope.layout) { - return $log.error('angular-deckgrid: No CSS configuration found (see ' + - 'https://github.com/akoenig/angular-deckgrid#the-grid-configuration)'); - } - - this.$$scope.columns = []; - - angular.forEach(this.$$scope.model, function onIteration (card, index) { - var column = (index % self.$$scope.layout.columns) | 0; - - if (!self.$$scope.columns[column]) { - self.$$scope.columns[column] = []; - } - - card.$index = index; - self.$$scope.columns[column].push(card); - }); - }; - - /** - * @private - * - * Parses the configuration out of the configured CSS styles. - * - * Example: - * - * .deckgrid::before { + '$window', + '$log', + + function initialize($window, $log) { + + 'use strict'; + + /** + * The deckgrid directive. + * + */ + function Deckgrid(scope, element) { + var self = this, + watcher, + mql; + + this.$$elem = element; + this.$$watchers = []; + + this.$$scope = scope; + this.$$scope.columns = []; + + // + // The layout configuration will be parsed from + // the pseudo "before element." There you have to save all + // the column configurations. + // + this.$$scope.layout = this.$$getLayout(); + + this.$$createColumns(); + + // + // Register model change. + // + watcher = this.$$scope.$watchCollection('model', this.$$onModelChange.bind(this)); + this.$$watchers.push(watcher); + + // + // Register media query change events. + // + angular.forEach(self.$$getMediaQueries(), function onIteration(rule) { + var handler = self.$$onMediaQueryChange.bind(self); + + function onDestroy() { + rule.removeListener(handler); + } + + rule.addListener(handler); + + self.$$watchers.push(onDestroy); + }); + + mql = $window.matchMedia('(orientation: portrait)'); + mql.addListener(self.$$onMediaQueryChange.bind(self)); + + } + + /** + * @private + * + * Extracts the media queries out of the stylesheets. + * + * This method will fetch the media queries out of the stylesheets that are + * responsible for styling the angular-deckgrid. + * + * @return {array} An array with all respective styles. + * + */ + Deckgrid.prototype.$$getMediaQueries = function $$getMediaQueries() { + var stylesheets = [], + mediaQueries = []; + + stylesheets = Array.prototype.concat.call( + Array.prototype.slice.call(document.querySelectorAll('style[type=\'text/css\']')), + Array.prototype.slice.call(document.querySelectorAll('link[rel=\'stylesheet\']')) + ); + + function extractRules(stylesheet) { + try { + return (stylesheet.sheet.cssRules || []); + } catch (e) { + return []; + } + } + + function hasDeckgridStyles(rule) { + var regexe = /\[(\w*-)?deckgrid\]::?before/g, + i = 0, + selector = ''; + + if (!rule.media || angular.isUndefined(rule.cssRules)) { + return false; + } + + i = rule.cssRules.length - 1; + + for (i; i >= 0; i = i - 1) { + selector = rule.cssRules[i].selectorText; + + if (angular.isDefined(selector) && selector.match(regexe)) { + return true; + } + } + + return false; + } + + angular.forEach(stylesheets, function onIteration(stylesheet) { + var rules = extractRules(stylesheet); + + angular.forEach(rules, function inRuleIteration(rule) { + if (hasDeckgridStyles(rule)) { + mediaQueries.push($window.matchMedia(rule.media.mediaText)); + } + }); + }); + + return mediaQueries; + }; + + Deckgrid.prototype.$$cachedParams = function $$cachedParams(column) { + if (!this.$$cache) { + this.$$cache = {}; + } + if (this.$$cache.column === column) { + return this.$$cache; + } + this.$$cache = { column: column, modelLength: 0 }; + return this.$$cache; + }; + + /** + * @private + * + * Creates the column segmentation. With other words: + * This method creates the internal data structure from the + * passed "source" attribute. Every card within this "source" + * model will be passed into this internal column structure by + * reference. So if you modify the data within your controller + * this directive will reflect these changes immediately. + * + * NOTE that calling this method will trigger a complete template "redraw". + * + */ + Deckgrid.prototype.$$createColumns = function $$createColumns() { + if (!this.$$scope.layout) { + return $log.error('angular-deckgrid: No CSS configuration found (see ' + + 'https://github.com/akoenig/angular-deckgrid#the-grid-configuration)'); + } + + var cachedParams = this.$$cachedParams(this.$$scope.layout.columns); + if (cachedParams.modelLength === 0) { + // layout change, reset columns + this.$$scope.columns = []; + } + + var modelLength = 0; + if (this.$$scope.model) { + modelLength = this.$$scope.model.length; + } + for (var index = cachedParams.modelLength; index < modelLength; index++) { + var card = this.$$scope.model[index]; + var column = (index % this.$$scope.layout.columns) | 0; + + if (!this.$$scope.columns[column]) { + this.$$scope.columns[column] = []; + } + card.$index = index; + this.$$scope.columns[column].push(card); + } + + cachedParams.modelLength = modelLength; + }; + + /** + * @private + * + * Parses the configuration out of the configured CSS styles. + * + * Example: + * + * .deckgrid::before { * content: '3 .column.size-1-3'; * } - * - * Will result in a three column grid where each column will have the - * classes: "column size-1-3". - * - * You are responsible for defining the respective styles within your CSS. - * - */ - Deckgrid.prototype.$$getLayout = function $$getLayout () { - var content = $window.getComputedStyle(this.$$elem, ':before').content, - layout; - - if (content) { - content = content.replace(/'/g, ''); // before e.g. '3 .column.size-1of3' - content = content.replace(/"/g, ''); // before e.g. "3 .column.size-1of3" - content = content.split(' '); - - if (2 === content.length) { - layout = {}; - layout.columns = (content[0] | 0); - layout.classList = content[1].replace(/\./g, ' ').trim(); - } - } - - return layout; - }; - - /** - * @private - * - * Event that will be triggered if a CSS media query changed. - * - */ - Deckgrid.prototype.$$onMediaQueryChange = function $$onMediaQueryChange () { - var self = this, - layout = this.$$getLayout(); - - // - // Okay, the layout has changed. - // Creating a new column structure is not avoidable. - // - if (layout.columns !== this.$$scope.layout.columns) { - self.$$scope.layout = layout; - - self.$$scope.$apply(function onApply () { - self.$$createColumns(); - }); - } - }; - - /** - * @private - * - * Event that will be triggered when the source model has changed. - * - */ - Deckgrid.prototype.$$onModelChange = function $$onModelChange (newModel, oldModel) { - var self = this; - - newModel = newModel || []; - oldModel = oldModel || []; - - if (oldModel.length !== newModel.length) { - self.$$createColumns(); - } - }; - - /** - * Destroys the directive. Takes care of cleaning all - * watchers and event handlers. - * - */ - Deckgrid.prototype.destroy = function destroy () { - var i = this.$$watchers.length - 1; - - for (i; i >= 0; i = i - 1) { - this.$$watchers[i](); - } - }; - - return { - create : function create (scope, element) { - return new Deckgrid(scope, element); - } - }; - } + * + * Will result in a three column grid where each column will have the + * classes: "column size-1-3". + * + * You are responsible for defining the respective styles within your CSS. + * + */ + Deckgrid.prototype.$$getLayout = function $$getLayout() { + var content = $window.getComputedStyle(this.$$elem, ':before').content, + layout; + + if (content) { + content = content.replace(/'/g, ''); // before e.g. '3 .column.size-1of3' + content = content.replace(/"/g, ''); // before e.g. "3 .column.size-1of3" + content = content.split(' '); + + if (2 === content.length) { + layout = {}; + layout.columns = (content[0] | 0); + layout.classList = content[1].replace(/\./g, ' ').trim(); + } + } + + return layout; + }; + + /** + * @private + * + * Event that will be triggered if a CSS media query changed. + * + */ + Deckgrid.prototype.$$onMediaQueryChange = function $$onMediaQueryChange() { + var self = this, + layout = this.$$getLayout(); + + // + // Okay, the layout has changed. + // Creating a new column structure is not avoidable. + // + if (layout.columns !== this.$$scope.layout.columns) { + self.$$scope.layout = layout; + + self.$$scope.$apply(function onApply() { + self.$$createColumns(); + }); + } + }; + + /** + * @private + * + * Event that will be triggered when the source model has changed. + * + */ + Deckgrid.prototype.$$onModelChange = function $$onModelChange(newModel, oldModel) { + var self = this; + + newModel = newModel || []; + oldModel = oldModel || []; + + if (oldModel.length !== newModel.length) { + self.$$createColumns(); + } else { + var i = newModel.length - 1; + for (i; i >= 0; i = i - 1) { + if (oldModel[i] !== newModel[i]) { + self.$$createColumns(); + break; + } + } + } + }; + + /** + * Destroys the directive. Takes care of cleaning all + * watchers and event handlers. + * + */ + Deckgrid.prototype.destroy = function destroy() { + var i = this.$$watchers.length - 1; + + for (i; i >= 0; i = i - 1) { + this.$$watchers[i](); + } + }; + + return { + create: function create(scope, element) { + return new Deckgrid(scope, element); + } + }; + } ]); diff --git a/src/descriptor.js b/src/descriptor.js index 8fdad69..175b6f7 100644 --- a/src/descriptor.js +++ b/src/descriptor.js @@ -73,8 +73,8 @@ angular.module('akoenig.deckgrid').factory('DeckgridDescriptor', [ scope.$on('$destroy', this.$$destroy.bind(this)); - if (attrs.cardtemplate === undefined) { - if (attrs.cardtemplatestring === undefined) { + if (angular.isUndefined(attrs.cardtemplate)) { + if (angular.isUndefined(attrs.cardtemplatestring)) { // use the provided inner html as template transclude(scope, function onTransclude (innerHTML) { var extractedInnerHTML = [], @@ -85,7 +85,7 @@ angular.module('akoenig.deckgrid').factory('DeckgridDescriptor', [ for (i; i < len; i = i + 1) { outerHTML = innerHTML[i].outerHTML; - if (outerHTML !== undefined) { + if (angular.isDefined(outerHTML)) { extractedInnerHTML.push(outerHTML); } }