From 82b2b443fab220cd9ac7d3a8c90c1edc4291e54a Mon Sep 17 00:00:00 2001 From: TravisTX Date: Sat, 4 Apr 2015 17:19:39 -0400 Subject: [PATCH] feat(ncyBreadcrumbText): Add ncyBreadcrumbText directive --- dist/angular-breadcrumb.js | 94 ++++++++++++++++++- dist/angular-breadcrumb.min.js | 4 +- sample/index.html | 2 +- src/angular-breadcrumb.js | 88 ++++++++++++++++- .../directive-text-basic-separator-test.js | 53 +++++++++++ test/spec/directive-text-basic-test.js | 53 +++++++++++ .../spec/directive-text-interpolation-test.js | 56 +++++++++++ test/spec/directive-text-sample-test.js | 34 +++++++ 8 files changed, 376 insertions(+), 8 deletions(-) create mode 100644 test/spec/directive-text-basic-separator-test.js create mode 100644 test/spec/directive-text-basic-test.js create mode 100644 test/spec/directive-text-interpolation-test.js create mode 100644 test/spec/directive-text-sample-test.js diff --git a/dist/angular-breadcrumb.js b/dist/angular-breadcrumb.js index 5c778ef..d230d71 100644 --- a/dist/angular-breadcrumb.js +++ b/dist/angular-breadcrumb.js @@ -1,4 +1,4 @@ -/*! angular-breadcrumb - v0.3.3-dev-2015-03-27 +/*! angular-breadcrumb - v0.3.3-dev-2015-04-04 * http://ncuillery.github.io/angular-breadcrumb * Copyright (c) 2015 Nicolas Cuillery; Licensed MIT */ @@ -183,14 +183,14 @@ function BreadcrumbDirective($interpolate, $breadcrumb, $rootScope) { var $$templates = { bootstrap2: '', bootstrap3: '' @@ -284,8 +284,94 @@ function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { } BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; +function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { + + return { + restrict: 'A', + scope: {}, + template: '{{ncyBreadcrumbLabel}}', + + compile: function(cElement, cAttrs) { + // Override the default template if ncyBreadcrumbText has a value + var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); + if(template) { + cElement.html(template); + } + + var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator); + if (!separator) { + separator = ' / '; + } + + return { + post: function postLink(scope) { + var labelWatchers = []; + + var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope, step) { + angular.forEach(getExpression(interpolationFunction), function(expression) { + var watcher = viewScope.$watch(expression, function() { + step.ncyBreadcrumbLabel = interpolationFunction(viewScope); + renderLabel(); + }); + labelWatcherArray.push(watcher); + }); + }; + + var setupWatchers = function() { + deregisterWatchers(labelWatchers); + + var viewScope = $breadcrumb.$getLastViewScope(); + var steps = $breadcrumb.getStatesChain(); + angular.forEach(steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + // Watcher for further viewScope updates + registerWatchersText(labelWatchers, parseLabel, viewScope, step); + } + }); + + renderLabel(); + }; + + var renderLabel = function() { + var viewScope = $breadcrumb.$getLastViewScope(); + var steps = $breadcrumb.getStatesChain(); + var combinedLabel = ''; + angular.forEach(steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + combinedLabel += separator + parseLabel(viewScope); + } else { + combinedLabel += separator + step.name; + } + }); + + if (combinedLabel.length > 3) { + combinedLabel = combinedLabel.substring(3); + } + + scope.ncyBreadcrumbLabel = combinedLabel; + }; + + $rootScope.$on('$viewContentLoaded', function () { + setupWatchers(); + renderLabel(); + }); + + // View(s) may be already loaded while the directive's linking + setupWatchers(); + renderLabel(); + } + }; + + } + }; +} +BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + angular.module('ncy-angular-breadcrumb', ['ui.router.state']) .provider('$breadcrumb', $Breadcrumb) .directive('ncyBreadcrumb', BreadcrumbDirective) - .directive('ncyBreadcrumbLast', BreadcrumbLastDirective); + .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) + .directive('ncyBreadcrumbText', BreadcrumbTextDirective); })(window, window.angular); diff --git a/dist/angular-breadcrumb.min.js b/dist/angular-breadcrumb.min.js index 0352fe3..6ddedf0 100644 --- a/dist/angular-breadcrumb.min.js +++ b/dist/angular-breadcrumb.min.js @@ -1,4 +1,4 @@ -/*! angular-breadcrumb - v0.3.3-dev-2015-03-27 +/*! angular-breadcrumb - v0.3.3-dev-2015-04-04 * http://ncuillery.github.io/angular-breadcrumb * Copyright (c) 2015 Nicolas Cuillery; Licensed MIT */ -!function(a,b,c){"use strict";function d(a,c){return b.equals(a.length,c.length)?a>c:a.length>c.length}function e(a){var b=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/);if(!b||4!==b.length)throw new Error("Invalid state ref '"+a+"'");return{state:b[1],paramExpr:b[3]||null}}function f(){var a={prefixStateName:null,template:"bootstrap3",templateUrl:null,includeAbstract:!1};this.setOptions=function(c){b.extend(a,c)},this.$get=["$state","$stateParams","$rootScope",function(b,f,g){var h=g;g.$on("$viewContentLoaded",function(a){d(a.targetScope.$id,h.$id)&&(h=a.targetScope)});var i=function(a){var b=a.parent||(/^(.+)\.[^.]+$/.exec(a.name)||[])[1];return b},j=function(c,d){for(var g,i,j=e(d),k=!1,l=!1,m=0,n=c.length;n>m;m+=1)if(c[m].name===j.state)return;g=b.get(j.state),g.ncyBreadcrumb&&(g.ncyBreadcrumb.force&&(k=!0),g.ncyBreadcrumb.skip&&(l=!0)),g["abstract"]&&!a.includeAbstract&&!k||l||(j.paramExpr&&(i=h.$eval(j.paramExpr)),g.ncyBreadcrumbLink=b.href(j.state,i||f||{}),c.unshift(g))},k=function(a){var c=e(a),d=b.get(c.state);if(d.ncyBreadcrumb&&d.ncyBreadcrumb.parent){var f="function"==typeof d.ncyBreadcrumb.parent,g=f?d.ncyBreadcrumb.parent(h):d.ncyBreadcrumb.parent;if(g)return g}return i(d)};return{getTemplate:function(b){return a.templateUrl?null:b[a.template]?b[a.template]:a.template},getTemplateUrl:function(){return a.templateUrl},getStatesChain:function(c){for(var d=[],e=b.$current.self.name;e;e=k(e))if(j(d,e),c&&d.length)return d;return a.prefixStateName&&j(d,a.prefixStateName),d},getLastStep:function(){var a=this.getStatesChain(!0);return a.length?a[0]:c},$getLastViewScope:function(){return h}}}]}function g(a,c,d){var e={bootstrap2:'',bootstrap3:''};return{restrict:"AE",replace:!0,scope:{},template:c.getTemplate(e),templateUrl:c.getTemplateUrl(),link:{post:function(e){var f=[],g=function(){k(f);var d=c.$getLastViewScope();e.steps=c.getStatesChain(),b.forEach(e.steps,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);b.ncyBreadcrumbLabel=c(d),j(f,c,d,b)}else b.ncyBreadcrumbLabel=b.name})};d.$on("$viewContentLoaded",function(){g()}),g()}}}}function h(a,b,c){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbLabel}}",compile:function(d,e){var f=d.attr(e.$attr.ncyBreadcrumbLast);return f&&d.html(f),{post:function(d){var e=[],f=function(){k(e);var c=b.$getLastViewScope(),f=b.getLastStep();if(f)if(d.ncyBreadcrumbLink=f.ncyBreadcrumbLink,f.ncyBreadcrumb&&f.ncyBreadcrumb.label){var g=a(f.ncyBreadcrumb.label);d.ncyBreadcrumbLabel=g(c),j(e,g,c,d)}else d.ncyBreadcrumbLabel=f.name};c.$on("$viewContentLoaded",function(){f()}),f()}}}}}var i=function(a){if(a.expressions)return a.expressions;var c=[];return b.forEach(a.parts,function(a){b.isFunction(a)&&c.push(a.exp)}),c},j=function(a,c,d,e){b.forEach(i(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d)});a.push(f)})},k=function(a){b.forEach(a,function(a){a()}),a=[]};g.$inject=["$interpolate","$breadcrumb","$rootScope"],h.$inject=["$interpolate","$breadcrumb","$rootScope"],b.module("ncy-angular-breadcrumb",["ui.router.state"]).provider("$breadcrumb",f).directive("ncyBreadcrumb",g).directive("ncyBreadcrumbLast",h)}(window,window.angular); \ No newline at end of file +!function(a,b,c){"use strict";function d(a,c){return b.equals(a.length,c.length)?a>c:a.length>c.length}function e(a){var b=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/);if(!b||4!==b.length)throw new Error("Invalid state ref '"+a+"'");return{state:b[1],paramExpr:b[3]||null}}function f(){var a={prefixStateName:null,template:"bootstrap3",templateUrl:null,includeAbstract:!1};this.setOptions=function(c){b.extend(a,c)},this.$get=["$state","$stateParams","$rootScope",function(b,f,g){var h=g;g.$on("$viewContentLoaded",function(a){d(a.targetScope.$id,h.$id)&&(h=a.targetScope)});var i=function(a){var b=a.parent||(/^(.+)\.[^.]+$/.exec(a.name)||[])[1];return b},j=function(c,d){for(var g,i,j=e(d),k=!1,l=!1,m=0,n=c.length;n>m;m+=1)if(c[m].name===j.state)return;g=b.get(j.state),g.ncyBreadcrumb&&(g.ncyBreadcrumb.force&&(k=!0),g.ncyBreadcrumb.skip&&(l=!0)),g["abstract"]&&!a.includeAbstract&&!k||l||(j.paramExpr&&(i=h.$eval(j.paramExpr)),g.ncyBreadcrumbLink=b.href(j.state,i||f||{}),c.unshift(g))},k=function(a){var c=e(a),d=b.get(c.state);if(d.ncyBreadcrumb&&d.ncyBreadcrumb.parent){var f="function"==typeof d.ncyBreadcrumb.parent,g=f?d.ncyBreadcrumb.parent(h):d.ncyBreadcrumb.parent;if(g)return g}return i(d)};return{getTemplate:function(b){return a.templateUrl?null:b[a.template]?b[a.template]:a.template},getTemplateUrl:function(){return a.templateUrl},getStatesChain:function(c){for(var d=[],e=b.$current.self.name;e;e=k(e))if(j(d,e),c&&d.length)return d;return a.prefixStateName&&j(d,a.prefixStateName),d},getLastStep:function(){var a=this.getStatesChain(!0);return a.length?a[0]:c},$getLastViewScope:function(){return h}}}]}function g(a,c,d){var e={bootstrap2:'',bootstrap3:''};return{restrict:"AE",replace:!0,scope:{},template:c.getTemplate(e),templateUrl:c.getTemplateUrl(),link:{post:function(e){var f=[],g=function(){l(f);var d=c.$getLastViewScope();e.steps=c.getStatesChain(),b.forEach(e.steps,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);b.ncyBreadcrumbLabel=c(d),k(f,c,d,b)}else b.ncyBreadcrumbLabel=b.name})};d.$on("$viewContentLoaded",function(){g()}),g()}}}}function h(a,b,c){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbLabel}}",compile:function(d,e){var f=d.attr(e.$attr.ncyBreadcrumbLast);return f&&d.html(f),{post:function(d){var e=[],f=function(){l(e);var c=b.$getLastViewScope(),f=b.getLastStep();if(f)if(d.ncyBreadcrumbLink=f.ncyBreadcrumbLink,f.ncyBreadcrumb&&f.ncyBreadcrumb.label){var g=a(f.ncyBreadcrumb.label);d.ncyBreadcrumbLabel=g(c),k(e,g,c,d)}else d.ncyBreadcrumbLabel=f.name};c.$on("$viewContentLoaded",function(){f()}),f()}}}}}function i(a,c,d){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbLabel}}",compile:function(e,f){var g=e.attr(f.$attr.ncyBreadcrumbText);g&&e.html(g);var h=e.attr(f.$attr.ncyBreadcrumbTextSeparator);return h||(h=" / "),{post:function(e){var f=[],g=function(a,c,d,e){b.forEach(j(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d),k()});a.push(f)})},i=function(){l(f);var d=c.$getLastViewScope(),e=c.getStatesChain();b.forEach(e,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);g(f,c,d,b)}}),k()},k=function(){var d=c.$getLastViewScope(),f=c.getStatesChain(),g="";b.forEach(f,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);g+=h+c(d)}else g+=h+b.name}),g.length>3&&(g=g.substring(3)),e.ncyBreadcrumbLabel=g};d.$on("$viewContentLoaded",function(){i(),k()}),i(),k()}}}}}var j=function(a){if(a.expressions)return a.expressions;var c=[];return b.forEach(a.parts,function(a){b.isFunction(a)&&c.push(a.exp)}),c},k=function(a,c,d,e){b.forEach(j(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d)});a.push(f)})},l=function(a){b.forEach(a,function(a){a()}),a=[]};g.$inject=["$interpolate","$breadcrumb","$rootScope"],h.$inject=["$interpolate","$breadcrumb","$rootScope"],i.$inject=["$interpolate","$breadcrumb","$rootScope"],b.module("ncy-angular-breadcrumb",["ui.router.state"]).provider("$breadcrumb",f).directive("ncyBreadcrumb",g).directive("ncyBreadcrumbLast",h).directive("ncyBreadcrumbText",i)}(window,window.angular); \ No newline at end of file diff --git a/sample/index.html b/sample/index.html index e656a3a..717caea 100644 --- a/sample/index.html +++ b/sample/index.html @@ -13,7 +13,7 @@ - + diff --git a/src/angular-breadcrumb.js b/src/angular-breadcrumb.js index 14bb7ca..a30a5d9 100644 --- a/src/angular-breadcrumb.js +++ b/src/angular-breadcrumb.js @@ -279,7 +279,93 @@ function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { } BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; +function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { + + return { + restrict: 'A', + scope: {}, + template: '{{ncyBreadcrumbLabel}}', + + compile: function(cElement, cAttrs) { + // Override the default template if ncyBreadcrumbText has a value + var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); + if(template) { + cElement.html(template); + } + + var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator); + if (!separator) { + separator = ' / '; + } + + return { + post: function postLink(scope) { + var labelWatchers = []; + + var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope, step) { + angular.forEach(getExpression(interpolationFunction), function(expression) { + var watcher = viewScope.$watch(expression, function() { + step.ncyBreadcrumbLabel = interpolationFunction(viewScope); + renderLabel(); + }); + labelWatcherArray.push(watcher); + }); + }; + + var setupWatchers = function() { + deregisterWatchers(labelWatchers); + + var viewScope = $breadcrumb.$getLastViewScope(); + var steps = $breadcrumb.getStatesChain(); + angular.forEach(steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + // Watcher for further viewScope updates + registerWatchersText(labelWatchers, parseLabel, viewScope, step); + } + }); + + renderLabel(); + }; + + var renderLabel = function() { + var viewScope = $breadcrumb.$getLastViewScope(); + var steps = $breadcrumb.getStatesChain(); + var combinedLabel = ''; + angular.forEach(steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + combinedLabel += separator + parseLabel(viewScope); + } else { + combinedLabel += separator + step.name; + } + }); + + if (combinedLabel.length > 3) { + combinedLabel = combinedLabel.substring(3); + } + + scope.ncyBreadcrumbLabel = combinedLabel; + }; + + $rootScope.$on('$viewContentLoaded', function () { + setupWatchers(); + renderLabel(); + }); + + // View(s) may be already loaded while the directive's linking + setupWatchers(); + renderLabel(); + } + }; + + } + }; +} +BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + angular.module('ncy-angular-breadcrumb', ['ui.router.state']) .provider('$breadcrumb', $Breadcrumb) .directive('ncyBreadcrumb', BreadcrumbDirective) - .directive('ncyBreadcrumbLast', BreadcrumbLastDirective); + .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) + .directive('ncyBreadcrumbText', BreadcrumbTextDirective); diff --git a/test/spec/directive-text-basic-separator-test.js b/test/spec/directive-text-basic-separator-test.js new file mode 100644 index 0000000..677b620 --- /dev/null +++ b/test/spec/directive-text-basic-separator-test.js @@ -0,0 +1,53 @@ +/*jshint undef: false */ + +describe('Text directive with separator with basic conf', function() { + + var element, scope; + + beforeEach(function() { + module('ncy-basic-conf'); + }); + + describe('without template', function() { + + beforeEach(inject(function($rootScope, $compile) { + element = angular.element(''); + var compile = $compile(element); + scope = $rootScope.$new(); + compile(scope); + scope.$digest(); + })); + + it('renders the text label correctly', inject(function() { + goToState('D'); + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + expect(element.text()).toBe('State A > State B > State C > State D'); + })); + + }); + + describe('with template', function() { + + beforeEach(inject(function($rootScope, $compile) { + element = angular.element(''); + var compile = $compile(element); + scope = $rootScope.$new(); + compile(scope); + scope.$digest(); + })); + + it('renders the template correctly', inject(function() { + goToState('D'); + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + expect(element.text()).toBe('State A > State B > State C > State D - MyApp'); + })); + + }); + +}); diff --git a/test/spec/directive-text-basic-test.js b/test/spec/directive-text-basic-test.js new file mode 100644 index 0000000..b6b7b1d --- /dev/null +++ b/test/spec/directive-text-basic-test.js @@ -0,0 +1,53 @@ +/*jshint undef: false */ + +describe('Text directive with basic conf', function() { + + var element, scope; + + beforeEach(function() { + module('ncy-basic-conf'); + }); + + describe('without template', function() { + + beforeEach(inject(function($rootScope, $compile) { + element = angular.element(''); + var compile = $compile(element); + scope = $rootScope.$new(); + compile(scope); + scope.$digest(); + })); + + it('renders the text label correctly', inject(function() { + goToState('D'); + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + expect(element.text()).toBe('State A / State B / State C / State D'); + })); + + }); + + describe('with template', function() { + + beforeEach(inject(function($rootScope, $compile) { + element = angular.element(''); + var compile = $compile(element); + scope = $rootScope.$new(); + compile(scope); + scope.$digest(); + })); + + it('renders the template correctly', inject(function() { + goToState('D'); + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + expect(element.text()).toBe('State A / State B / State C / State D - MyApp'); + })); + + }); + +}); diff --git a/test/spec/directive-text-interpolation-test.js b/test/spec/directive-text-interpolation-test.js new file mode 100644 index 0000000..7c00bc0 --- /dev/null +++ b/test/spec/directive-text-interpolation-test.js @@ -0,0 +1,56 @@ +/*jshint undef: false */ + +describe('Text directive with interpolation conf', function() { + + var element, scope, controller, compile; + + beforeEach(function() { + module('ncy-interpolation-conf'); + }); + + beforeEach(inject(function($rootScope, $compile, $controller) { + element = angular.element(''); + compile = $compile(element); + scope = $rootScope.$new(); + controller = $controller; + })); + + it('interpolates labels correctly', inject(function() { + goToState('A.B'); + + controller('BCtrl', {'$scope' : scope} ); + compile(scope); + + expect(scope.tripleB).toBeDefined(); + + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + + expect(element.text()).toBe('test|State A / State BBB'); + })); + + it('deals with further updates of the scope', inject(function() { + goToState('A.B'); + + controller('BCtrl', {'$scope' : scope} ); + compile(scope); + + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + + expect(element.text()).toBe('test|State A / State BBB'); + + scope.tripleB = 'HACKED'; + scope.$digest(); + + expect(element.text()).toBe('test|State A / State HACKED'); + + })); + + + +}); diff --git a/test/spec/directive-text-sample-test.js b/test/spec/directive-text-sample-test.js new file mode 100644 index 0000000..04a5880 --- /dev/null +++ b/test/spec/directive-text-sample-test.js @@ -0,0 +1,34 @@ +/*jshint undef: false */ + +describe('Text directive with sample conf', function() { + + var element, scope, controller, compile; + + beforeEach(function() { + module('ncy-sample-conf'); + }); + + beforeEach(inject(function($rootScope, $compile, $controller) { + element = angular.element(''); + compile = $compile(element); + scope = $rootScope.$new(); + controller = $controller; + })); + + it('interpolates "room.detail" label correctly', inject(function() { + goToStateAndFlush('room.detail', {roomId: 3}); + + controller('RoomDetailCtrl', {'$scope' : scope} ); + compile(scope); + + expect(scope.room).toBeDefined(); + + scope.$emit('$viewContentLoaded'); + scope.$digest(); + + console.info('Directive content : ' + element.text()); + + expect(element.text()).toBe('MyApp: Home / Sample / Rooms / Room 103'); + })); + +});