angular-breadcrumb.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /*! angular-breadcrumb - v0.3.2-dev-2014-12-14
  2. * http://ncuillery.github.io/angular-breadcrumb
  3. * Copyright (c) 2014 Nicolas Cuillery; Licensed MIT */
  4. (function (window, angular, undefined) {
  5. 'use strict';
  6. function isAOlderThanB(scopeA, scopeB) {
  7. if(angular.equals(scopeA.length, scopeB.length)) {
  8. return scopeA > scopeB;
  9. } else {
  10. return scopeA.length > scopeB.length;
  11. }
  12. }
  13. function parseStateRef(ref) {
  14. var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
  15. if (!parsed || parsed.length !== 4) { throw new Error("Invalid state ref '" + ref + "'"); }
  16. return { state: parsed[1], paramExpr: parsed[3] || null };
  17. }
  18. function $Breadcrumb() {
  19. var $$options = {
  20. prefixStateName: null,
  21. template: 'bootstrap3',
  22. templateUrl: null,
  23. includeAbstract : false
  24. };
  25. this.setOptions = function(options) {
  26. angular.extend($$options, options);
  27. };
  28. this.$get = ['$state', '$stateParams', '$rootScope', function($state, $stateParams, $rootScope) {
  29. var $lastViewScope = $rootScope;
  30. // Early catch of $viewContentLoaded event
  31. $rootScope.$on('$viewContentLoaded', function (event) {
  32. // With nested views, the event occur several times, in "wrong" order
  33. if(isAOlderThanB(event.targetScope.$id, $lastViewScope.$id)) {
  34. $lastViewScope = event.targetScope;
  35. }
  36. });
  37. // Get the parent state
  38. var $$parentState = function(state) {
  39. // Check if state has explicit parent OR we try guess parent from its name
  40. var name = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1];
  41. // If we were able to figure out parent name then get this state
  42. return name;
  43. };
  44. // Add the state in the chain if not already in and if not abstract
  45. var $$addStateInChain = function(chain, stateRef) {
  46. var conf,
  47. parentParams,
  48. ref = parseStateRef(stateRef);
  49. for(var i=0, l=chain.length; i<l; i+=1) {
  50. if (chain[i].name === ref.state) {
  51. return;
  52. }
  53. }
  54. conf = $state.get(ref.state);
  55. if((!conf.abstract || $$options.includeAbstract) && !(conf.ncyBreadcrumb && conf.ncyBreadcrumb.skip)) {
  56. if(ref.paramExpr) {
  57. parentParams = $lastViewScope.$eval(ref.paramExpr);
  58. }
  59. conf.ncyBreadcrumbLink = $state.href(ref.state, parentParams || $stateParams || {});
  60. chain.unshift(conf);
  61. }
  62. };
  63. // Get the state for the parent step in the breadcrumb
  64. var $$breadcrumbParentState = function(stateRef) {
  65. var ref = parseStateRef(stateRef),
  66. conf = $state.get(ref.state);
  67. if(conf.ncyBreadcrumb && conf.ncyBreadcrumb.parent) {
  68. // Handle the "parent" property of the breadcrumb, override the parent/child relation of the state
  69. var isFunction = typeof conf.ncyBreadcrumb.parent === 'function';
  70. var parentStateRef = isFunction ? conf.ncyBreadcrumb.parent($lastViewScope) : conf.ncyBreadcrumb.parent;
  71. if(parentStateRef) {
  72. return parentStateRef;
  73. }
  74. }
  75. return $$parentState(conf);
  76. };
  77. return {
  78. getTemplate: function(templates) {
  79. if($$options.templateUrl) {
  80. // templateUrl takes precedence over template
  81. return null;
  82. } else if(templates[$$options.template]) {
  83. // Predefined templates (bootstrap, ...)
  84. return templates[$$options.template];
  85. } else {
  86. return $$options.template;
  87. }
  88. },
  89. getTemplateUrl: function() {
  90. return $$options.templateUrl;
  91. },
  92. getStatesChain: function(exitOnFirst) { // Deliberately undocumented param, see getLastStep
  93. var chain = [];
  94. // From current state to the root
  95. for(var stateRef = $state.$current.self.name; stateRef; stateRef=$$breadcrumbParentState(stateRef)) {
  96. $$addStateInChain(chain, stateRef);
  97. if(exitOnFirst && chain.length) {
  98. return chain;
  99. }
  100. }
  101. // Prefix state treatment
  102. if($$options.prefixStateName) {
  103. $$addStateInChain(chain, $$options.prefixStateName);
  104. }
  105. return chain;
  106. },
  107. getLastStep: function() {
  108. var chain = this.getStatesChain(true);
  109. return chain.length ? chain[0] : undefined;
  110. },
  111. $getLastViewScope: function() {
  112. return $lastViewScope;
  113. }
  114. };
  115. }];
  116. }
  117. var getExpression = function(interpolationFunction) {
  118. if(interpolationFunction.expressions) {
  119. return interpolationFunction.expressions;
  120. } else {
  121. var expressions = [];
  122. angular.forEach(interpolationFunction.parts, function(part) {
  123. if(angular.isFunction(part)) {
  124. expressions.push(part.exp);
  125. }
  126. });
  127. return expressions;
  128. }
  129. };
  130. var registerWatchers = function(labelWatcherArray, interpolationFunction, viewScope, step) {
  131. angular.forEach(getExpression(interpolationFunction), function(expression) {
  132. var watcher = viewScope.$watch(expression, function() {
  133. step.ncyBreadcrumbLabel = interpolationFunction(viewScope);
  134. });
  135. labelWatcherArray.push(watcher);
  136. });
  137. };
  138. var deregisterWatchers = function(labelWatcherArray) {
  139. angular.forEach(labelWatcherArray, function(deregisterWatch) {
  140. deregisterWatch();
  141. });
  142. labelWatcherArray = [];
  143. };
  144. function BreadcrumbDirective($interpolate, $breadcrumb, $rootScope) {
  145. var $$templates = {
  146. bootstrap2: '<ul class="breadcrumb">' +
  147. '<li ng-repeat="step in steps" ng-switch="$last || !!step.abstract" ng-class="{active: $last}">' +
  148. '<a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}">{{step.ncyBreadcrumbLabel}}</a> ' +
  149. '<span ng-switch-when="true">{{step.ncyBreadcrumbLabel}}</span>' +
  150. '<span class="divider" ng-hide="$last">/</span>' +
  151. '</li>' +
  152. '</ul>',
  153. bootstrap3: '<ol class="breadcrumb">' +
  154. '<li ng-repeat="step in steps" ng-class="{active: $last}" ng-switch="$last || !!step.abstract">' +
  155. '<a ng-switch-when="false" href="{{step.ncyBreadcrumbLink}}">{{step.ncyBreadcrumbLabel}}</a> ' +
  156. '<span ng-switch-when="true">{{step.ncyBreadcrumbLabel}}</span>' +
  157. '</li>' +
  158. '</ol>'
  159. };
  160. return {
  161. restrict: 'AE',
  162. replace: true,
  163. scope: {},
  164. template: $breadcrumb.getTemplate($$templates),
  165. templateUrl: $breadcrumb.getTemplateUrl(),
  166. link: {
  167. post: function postLink(scope) {
  168. var labelWatchers = [];
  169. var renderBreadcrumb = function() {
  170. deregisterWatchers(labelWatchers);
  171. var viewScope = $breadcrumb.$getLastViewScope();
  172. scope.steps = $breadcrumb.getStatesChain();
  173. angular.forEach(scope.steps, function (step) {
  174. if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) {
  175. var parseLabel = $interpolate(step.ncyBreadcrumb.label);
  176. step.ncyBreadcrumbLabel = parseLabel(viewScope);
  177. // Watcher for further viewScope updates
  178. registerWatchers(labelWatchers, parseLabel, viewScope, step);
  179. } else {
  180. step.ncyBreadcrumbLabel = step.name;
  181. }
  182. });
  183. };
  184. $rootScope.$on('$viewContentLoaded', function () {
  185. renderBreadcrumb();
  186. });
  187. // View(s) may be already loaded while the directive's linking
  188. renderBreadcrumb();
  189. }
  190. }
  191. };
  192. }
  193. BreadcrumbDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope'];
  194. function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) {
  195. return {
  196. restrict: 'A',
  197. scope: {},
  198. template: '{{ncyBreadcrumbLabel}}',
  199. compile: function(cElement, cAttrs) {
  200. // Override the default template if ncyBreadcrumbLast has a value
  201. var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbLast);
  202. if(template) {
  203. cElement.html(template);
  204. }
  205. return {
  206. post: function postLink(scope) {
  207. var labelWatchers = [];
  208. var renderLabel = function() {
  209. deregisterWatchers(labelWatchers);
  210. var viewScope = $breadcrumb.$getLastViewScope();
  211. var lastStep = $breadcrumb.getLastStep();
  212. if(lastStep) {
  213. scope.ncyBreadcrumbLink = lastStep.ncyBreadcrumbLink;
  214. if (lastStep.ncyBreadcrumb && lastStep.ncyBreadcrumb.label) {
  215. var parseLabel = $interpolate(lastStep.ncyBreadcrumb.label);
  216. scope.ncyBreadcrumbLabel = parseLabel(viewScope);
  217. // Watcher for further viewScope updates
  218. // Tricky last arg: the last step is the entire scope of the directive !
  219. registerWatchers(labelWatchers, parseLabel, viewScope, scope);
  220. } else {
  221. scope.ncyBreadcrumbLabel = lastStep.name;
  222. }
  223. }
  224. };
  225. $rootScope.$on('$viewContentLoaded', function () {
  226. renderLabel();
  227. });
  228. // View(s) may be already loaded while the directive's linking
  229. renderLabel();
  230. }
  231. };
  232. }
  233. };
  234. }
  235. BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope'];
  236. angular.module('ncy-angular-breadcrumb', ['ui.router.state'])
  237. .provider('$breadcrumb', $Breadcrumb)
  238. .directive('ncyBreadcrumb', BreadcrumbDirective)
  239. .directive('ncyBreadcrumbLast', BreadcrumbLastDirective);
  240. })(window, window.angular);