├── .bowerrc ├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── angular-breadcrumb.js └── angular-breadcrumb.min.js ├── karma.conf.js ├── package.js ├── package.json ├── release ├── angular-breadcrumb.js └── angular-breadcrumb.min.js ├── sample ├── angular-breadcrumb-0.5.0.zip ├── app.js ├── controllers │ ├── booking_day.js │ ├── booking_detail.js │ ├── booking_list.js │ ├── room_detail.js │ └── room_list.js ├── css │ └── sample.css ├── img │ ├── GitHub-Mark-16px.png │ ├── GitHub-Mark-24px.png │ ├── download-24px.png │ └── logo │ │ ├── angular-breadcrumb-logo-1200.png │ │ ├── angular-breadcrumb-logo-200.png │ │ ├── angular-breadcrumb-logo-3000.png │ │ ├── angular-breadcrumb-logo-400.png │ │ ├── angular-breadcrumb-logo-800.png │ │ └── angular-breadcrumb-logo-orig.png ├── index.html └── views │ ├── booking_day.html │ ├── booking_detail.html │ ├── booking_list.html │ ├── home.html │ ├── room_detail.html │ ├── room_form.html │ ├── room_list.html │ └── sample.html ├── src ├── .jshintrc └── angular-breadcrumb.js └── test ├── mock ├── test-logging.js ├── test-modules.js ├── test-ui-router-sample.js └── test-utils.js └── spec ├── conf-basic-test.js ├── conf-sample-test.js ├── conf-ui-router-sample-test.js ├── directive-basic-test.js ├── directive-dynamic-parent-test.js ├── directive-interpolation-test.js ├── directive-last-basic-test.js ├── directive-last-interpolation-test.js ├── directive-last-sample-test.js ├── directive-ncyBreadcrumbIgnore-test.js ├── directive-object-parent-test.js ├── directive-sample-test.js ├── directive-template-test.js ├── directive-text-basic-separator-test.js ├── directive-text-basic-test.js ├── directive-text-interpolation-test.js ├── directive-text-sample-test.js ├── directive-ui-sref-template-test.js ├── scope-compare-test.js ├── service-abstract-test.js ├── service-basic-test.js ├── service-dynamic-parent-test.js ├── service-sample-test.js └── service-ui-router-sample-test.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .idea 3 | /bower_components/ 4 | /coverage/ 5 | **~ 6 | sample/asset/* 7 | sample/img/glyphicons-halflings.png 8 | libpeerconnection.log 9 | /testDependencies/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | sample 2 | src 3 | test 4 | .idea 5 | bower_components 6 | coverage 7 | testDependencies 8 | .bowerrc 9 | .coveralls.yml 10 | .gitignore 11 | .jshintrc 12 | .travis.yml 13 | gruntfile.js 14 | karma.conf.js 15 | libpeerconnection.log 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - npm install -g bower grunt-cli 9 | - npm install 10 | - bower install -F 11 | 12 | script: 13 | - grunt test 14 | 15 | after_script: 16 | - cat coverage/*/lcov.info | ./node_modules/coveralls/bin/coveralls.js 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.5.0 (2016-11-14) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * Register listeners once to prevent scope being retained ([181f4c09](http://github.com/ncuillery/angular-breadcrumb/commit/181f4c0901007cbd72c7a9470cb9503eb6ab4d5a), closes [#98](http://github.com/ncuillery/angular-breadcrumb/issues/98)) 8 | 9 | 10 | #### Features 11 | 12 | * Expose ncyBreadcrumbStateRef for ui-sref usage in custom template ([56cec38b](http://github.com/ncuillery/angular-breadcrumb/commit/56cec38b1169ba91a766bb64f44ddff81d8af2a8), closes [#54](http://github.com/ncuillery/angular-breadcrumb/issues/54)) 13 | * **ncyBreadcrumbLast:** Add custom templating ([974f99b5](http://github.com/ncuillery/angular-breadcrumb/commit/974f99b584c85e45b1c0eb1acb4081bf103de06f), closes [#123](http://github.com/ncuillery/angular-breadcrumb/issues/123), [#125](http://github.com/ncuillery/angular-breadcrumb/issues/125)) 14 | 15 | 16 | 17 | ### 0.4.1 (2015-08-09) 18 | 19 | 20 | #### Features 21 | 22 | * add the scope-based ncyBreadcrumbIgnore flag ([934c5523](http://github.com/ncuillery/angular-breadcrumb/commit/934c5523208a9615d7cfa3abcb397bbe131332ac), closes [#42](http://github.com/ncuillery/angular-breadcrumb/issues/42), [#62](http://github.com/ncuillery/angular-breadcrumb/issues/42)) 23 | 24 | 25 | 26 | ### 0.4.0 (2015-05-17) 27 | 28 | 29 | #### Bug Fixes 30 | 31 | * **$breadcrumb:** Handle parents provided by StateObject references ([f4288d37](http://github.com/ncuillery/angular-breadcrumb/commit/f4288d375fd1090ffec1d67e85c6300d74d86d37), closes [#82](http://github.com/ncuillery/angular-breadcrumb/issues/82)) 32 | * **ncyBreadcrumb:** 33 | * Prevent memory leak when label is a binding ([264e10f6](http://github.com/ncuillery/angular-breadcrumb/commit/264e10f680e1bbb8d1e00cf500de39cac4222cfd), closes [#88](http://github.com/ncuillery/angular-breadcrumb/issues/88)) 34 | * Removed trailing spaces from breadcrumb items([bc276ed5](http://github.com/ncuillery/angular-breadcrumb/commit/bc276ed5351a586d4a6dc83ada0687e6ca485344), closes [#77](http://github.com/ncuillery/angular-breadcrumb/issues/77)) 35 | 36 | #### Features 37 | 38 | * Add force to ncyBreadcrumb options ([31125a38](http://github.com/ncuillery/angular-breadcrumb/commit/31125a386d706dd76df807b3b02e1fccea38fb59), closes [#78](http://github.com/ncuillery/angular-breadcrumb/issues/78)) 39 | * **ncyBreadcrumbText:** Add ncyBreadcrumbText directive ([82b2b443](http://github.com/ncuillery/angular-breadcrumb/commit/82b2b443fab220cd9ac7d3a8c90c1edc4291e54a), closes [#71](http://github.com/ncuillery/angular-breadcrumb/issues/71), [#83](http://github.com/ncuillery/angular-breadcrumb/issues/83)) 40 | 41 | 42 | 43 | ### 0.3.3 (2014-12-16) 44 | 45 | 46 | #### Bug Fixes 47 | 48 | * **ncyBreadcrumb:** define `$$templates` with var instead of attaching it to `window` ([c35c9d25](http://github.com/ncuillery/angular-breadcrumb/commit/c35c9d255b5e2585d225a961d1efdb51d18f6a55), closes [#55](http://github.com/ncuillery/angular-breadcrumb/issues/55)) 49 | 50 | 51 | 52 | ### 0.3.2 (2014-11-15) 53 | 54 | * **npm:** nothing, it's only a blank release due to a network problem during the last `npm publish` (f...ing npm doesn't allow a republish with the same version number [npm-registry-couchapp#148](https://github.com/npm/npm-registry-couchapp/issues/148)). 55 | 56 | 57 | ### 0.3.1 (2014-11-15) 58 | 59 | 60 | #### Bug Fixes 61 | 62 | * **npm:** update package.json after (unclean) npm publish ([ab8161c2](http://github.com/ncuillery/angular-breadcrumb/commit/ab8161c25f98613f725b5e5ff8fe147acd60b365), closes [#52](http://github.com/ncuillery/angular-breadcrumb/issues/52)) 63 | * **sample:** Send correct url params for the room link in booking view ([876de49a](http://github.com/ncuillery/angular-breadcrumb/commit/876de49a9c5d6e2d75714a606238e9041ed49baf)) 64 | 65 | 66 | 67 | ## 0.3.0 (2014-10-29) 68 | 69 | 70 | #### Bug Fixes 71 | 72 | * organize state-level options in `ncyBreadcrumb` key instead of `data` ([1ea436d3](http://github.com/ncuillery/angular-breadcrumb/commit/1ea436d3f6d5470b7ae3e71e71259dbd2422bc00), closes [#30](http://github.com/ncuillery/angular-breadcrumb/issues/30)) 73 | * curly braces appearing on title of sample app ([855e76cb](http://github.com/ncuillery/angular-breadcrumb/commit/855e76cb33fda607fa3caa230564b77b48262c40)) 74 | 75 | 76 | #### Features 77 | 78 | * Add a global option to include abstract states ([6f0461ea](http://github.com/ncuillery/angular-breadcrumb/commit/6f0461ea7db36d8e10c29ed10de1f1c08d215a19), closes [#35](http://github.com/ncuillery/angular-breadcrumb/issues/35), [#28](http://github.com/ncuillery/angular-breadcrumb/issues/28)) 79 | * **$breadcrumb:** 80 | * Support url params when using `ncyBreadcrumb.parent` property ([55730045](http://github.com/ncuillery/angular-breadcrumb/commit/55730045dcf3b4fb1048c67f1e18953505563ed4), closes [#46](http://github.com/ncuillery/angular-breadcrumb/issues/46)) 81 | * add the customization of the parent state with a function ([ada09015](http://github.com/ncuillery/angular-breadcrumb/commit/ada09015c49f05a94349dabf078f1ed621811aaa), closes [#32](http://github.com/ncuillery/angular-breadcrumb/issues/32)) 82 | * **ncyBreadcrumbLast:** Add a new directive rendering the last step ([1eef24fb](http://github.com/ncuillery/angular-breadcrumb/commit/1eef24fbe862a1e3308181c38f50755843cf4426), closes [#37](http://github.com/ncuillery/angular-breadcrumb/issues/37)) 83 | 84 | 85 | #### Breaking Changes 86 | 87 | * state-level options has been moved under the custom key 88 | `ncyBreadcrumb` in state's configuration. 89 | 90 | To migrate the code follow the example below: 91 | ``` 92 | // Before 93 | $stateProvider.state('A', { 94 | url: '/a', 95 | data: { 96 | ncyBreadcrumbLabel: 'State A' 97 | } 98 | }); 99 | ``` 100 | 101 | ``` 102 | // After 103 | $stateProvider.state('A', { 104 | url: '/a', 105 | ncyBreadcrumb: { 106 | label: 'State A' 107 | } 108 | }); 109 | ``` 110 | See [API reference](https://github.com/ncuillery/angular-breadcrumb/wiki/API-Reference) for more informations. 111 | ([1ea436d3](http://github.com/ncuillery/angular-breadcrumb/commit/1ea436d3f6d5470b7ae3e71e71259dbd2422bc00)) 112 | 113 | 114 | 115 | ### 0.2.3 (2014-07-26) 116 | 117 | 118 | #### Bug Fixes 119 | 120 | * **$breadcrumb:** use `$stateParams` in case of unhierarchical states ([1c3c05e0](http://github.com/ncuillery/angular-breadcrumb/commit/1c3c05e0acac191fe2e76db2ef18da339caefaaa), closes [#29](http://github.com/ncuillery/angular-breadcrumb/issues/29)) 121 | 122 | 123 | 124 | ### 0.2.2 (2014-06-23) 125 | 126 | 127 | #### Bug Fixes 128 | 129 | * catch the `$viewContentLoaded` earlier ([bb47dd54](http://github.com/ncuillery/angular-breadcrumb/commit/bb47dd54deb5efc579ccb9b1575e686803dee1c5), closes [#14](http://github.com/ncuillery/angular-breadcrumb/issues/14)) 130 | * **sample:** 131 | * make the CRU(D) about rooms working ([3ca89ec7](http://github.com/ncuillery/angular-breadcrumb/commit/3ca89ec771fd20dc4ab2d733612bdcfb96ced703)) 132 | * prevent direct URL access to a day disabled in the datepicker ([95236916](http://github.com/ncuillery/angular-breadcrumb/commit/95236916e00b19464a3dfe3584ef1b18da9ffb25), closes [#17](http://github.com/ncuillery/angular-breadcrumb/issues/17)) 133 | * use the same variable in the datepicker and from url params for state `booking.day` ([646f7060](http://github.com/ncuillery/angular-breadcrumb/commit/646f70607e494f0e5e3c2483ed69f689684b2742), closes [#16](http://github.com/ncuillery/angular-breadcrumb/issues/16)) 134 | 135 | 136 | #### Features 137 | 138 | * **ncyBreadcrumb:** watch every expression founded in labels ([1363515e](http://github.com/ncuillery/angular-breadcrumb/commit/1363515e20977ce2f39a1f5e5e1d701f0d7af296), closes [#20](http://github.com/ncuillery/angular-breadcrumb/issues/20)) 139 | 140 | 141 | 142 | ### 0.2.1 (2014-05-16) 143 | 144 | 145 | #### Bug Fixes 146 | 147 | * **$breadcrumb:** check if a state has a parent when looking for an inheritated property ([77e668b5](http://github.com/ncuillery/angular-breadcrumb/commit/77e668b5eb759570a64c2a885e81580953af3201), closes [#11](http://github.com/ncuillery/angular-breadcrumb/issues/11)) 148 | 149 | 150 | 151 | ### 0.2.0 (2014-05-08) 152 | 153 | 154 | #### Bug Fixes 155 | 156 | * **$breadcrumb:** remove abstract states from breadcrumb ([8a06c5ab](http://github.com/ncuillery/angular-breadcrumb/commit/8a06c5abce749027d48f7309d1aabea1e447dfd5), closes [#8](http://github.com/ncuillery/angular-breadcrumb/issues/8)) 157 | * **ncyBreadcrumb:** display the correct breadcrumb in case of direct access ([e1f455ba](http://github.com/ncuillery/angular-breadcrumb/commit/e1f455ba4def97d3fc76b53772867b5f9daf4232), closes [#10](http://github.com/ncuillery/angular-breadcrumb/issues/10)) 158 | 159 | 160 | #### Features 161 | 162 | * **$breadcrumb:** 163 | * add a configuration property for skipping a state in the breadcrumb ([dd255d90](http://github.com/ncuillery/angular-breadcrumb/commit/dd255d906c4231f44b48f066d4db197a9c6b9e27), closes [#9](http://github.com/ncuillery/angular-breadcrumb/issues/9)) 164 | * allow chain of states customization ([028e493a](http://github.com/ncuillery/angular-breadcrumb/commit/028e493a1ebcae5ae60b8a9d42b949262000d7df), closes [#7](http://github.com/ncuillery/angular-breadcrumb/issues/7)) 165 | * **ncyBreadcrumb:** add 'Element' declaration style '' ([b51441ea](http://github.com/ncuillery/angular-breadcrumb/commit/b51441eafb1659b782fea1f8668c7f455e1d6b4d)) 166 | 167 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to angular-breadcrumb 2 | 3 | I am very glad to see this project living with PR from contributors who trust in it. Here is some guidelines to keep the contributions useful and efficient. 4 | 5 | ## Development hints 6 | 7 | ### Installation 8 | - Checkout the repository 9 | - Run `npm install` 10 | - Run `bower install` 11 | 12 | ### Test running 13 | This module uses the classic AngularJS stack with: 14 | 15 | - Karma (test runner) 16 | - Jasmine (assertion framework) 17 | - angular-mocks (AngularJS module for testing) 18 | 19 | Run the test with the grunt task `grunt test`. It runs the tests with different versions of AngularJS. 20 | 21 | ### Test developing 22 | Tests are build around modules with a specific `$stateProvider` configuration: 23 | 24 | - [Basic configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L6): Basic definitions (no template, no controller) 25 | - [Interpolation configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L21): States with bindings in `ncyBreadcrumbLabel` 26 | - [HTML configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L36): States with HTML in `ncyBreadcrumbLabel` 27 | - [Sample configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L41): Bridge towards the sample app configuration for using in tests 28 | - [UI-router's configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-ui-router-sample.js#L9): Clone of the UI-router sample app (complemented with breadcrumb configuration) 29 | 30 | Theses modules are loaded by Karma and they are available in test specifications. 31 | 32 | Specifications are generally related to the directive `ncyBreadcrumb` or the service `$breadcrumb`. 33 | 34 | ### Sample 35 | If you are not familiar with JS testing. You can run the [sample](http://ncuillery.github.io/angular-breadcrumb/#/sample) locally for testing purposes by using `grunt sample`. Sources are live-reloaded after each changes. 36 | 37 | ## Submitting a Pull Request 38 | - Fork the [repository](https://github.com/ncuillery/angular-breadcrumb/) 39 | - Make your changes in a new git branch following the coding rules below. 40 | - Run the grunt default task (by typing `grunt` or `grunt default`): it will run the tests and build the module in `dist` directory) 41 | - Commit the changes (including the `dist` directory) by using the commit conventions explained below. 42 | - Push and make the PR 43 | 44 | 45 | ## Coding rules 46 | - When making changes on the source file, please check that your changes are covered by the tests. If not, create a new test case. 47 | 48 | 49 | ## Commit conventions 50 | angular-breadcrumb uses the same strict conventions as AngularJS and UI-router. These conventions are explained [here](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#-git-commit-guidelines). 51 | 52 | It is very important to fit these conventions especially for types `fix` and `feature` which are used by the CHANGELOG.md generation (it uses the [grunt-conventional-changelog](https://github.com/btford/grunt-conventional-changelog)). 53 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LIVERELOAD_PORT = 35729; 4 | var lrSnippet = require('connect-livereload')({ 5 | port: LIVERELOAD_PORT 6 | }); 7 | var mountFolder = function (connect, dir) { 8 | return connect.static(require('path').resolve(dir)); 9 | }; 10 | 11 | module.exports = function (grunt) { 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | // Metadata. 16 | pkg: grunt.file.readJSON('package.json'), 17 | headerDev: '/*! <%= pkg.name %> - v<%= pkg.version %>-dev-<%= grunt.template.today("yyyy-mm-dd") %>\n', 18 | headerRelease: '/*! <%= pkg.name %> - v<%= pkg.version %>\n', 19 | banner: '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 20 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 21 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', 22 | // Task configuration. 23 | concat: { 24 | dev: { 25 | options: { 26 | banner: '<%= headerDev %><%= banner %>\n(function (window, angular, undefined) {\n', 27 | footer: '})(window, window.angular);\n', 28 | stripBanners: true 29 | }, 30 | src: ['src/<%= pkg.name %>.js'], 31 | dest: 'dist/<%= pkg.name %>.js' 32 | }, 33 | release: { 34 | options: { 35 | banner: '<%= headerRelease %><%= banner %>\n(function (window, angular, undefined) {\n', 36 | footer: '})(window, window.angular);\n', 37 | stripBanners: true 38 | }, 39 | src: ['src/<%= pkg.name %>.js'], 40 | dest: 'release/<%= pkg.name %>.js' 41 | } 42 | }, 43 | uglify: { 44 | dev: { 45 | options: { 46 | banner: '<%= headerDev %><%= banner %>' 47 | }, 48 | src: '<%= concat.dev.dest %>', 49 | dest: 'dist/<%= pkg.name %>.min.js' 50 | }, 51 | release: { 52 | options: { 53 | banner: '<%= headerRelease %><%= banner %>' 54 | }, 55 | src: '<%= concat.release.dest %>', 56 | dest: 'release/<%= pkg.name %>.min.js' 57 | } 58 | }, 59 | karma: { 60 | unit: { 61 | configFile: 'karma.conf.js' 62 | } 63 | }, 64 | jshint: { 65 | options: { 66 | jshintrc: '.jshintrc' 67 | }, 68 | gruntfile: { 69 | src: 'Gruntfile.js' 70 | }, 71 | sources: { 72 | options: { 73 | jshintrc: 'src/.jshintrc' 74 | }, 75 | src: ['src/**/*.js'] 76 | }, 77 | test: { 78 | src: ['test/**/*.js'] 79 | } 80 | }, 81 | watch: { 82 | gruntfile: { 83 | files: '<%= jshint.gruntfile.src %>', 84 | tasks: ['jshint:gruntfile'] 85 | }, 86 | sources: { 87 | files: '<%= jshint.sources.src %>', 88 | tasks: ['jshint:sources', 'karma'] 89 | }, 90 | test: { 91 | files: '<%= jshint.test.src %>', 92 | tasks: ['jshint:test', 'karma'] 93 | }, 94 | sample: { 95 | options: { 96 | livereload: LIVERELOAD_PORT 97 | }, 98 | tasks: 'copy:breadcrumb', 99 | files: [ 100 | 'sample/*.{css,js,html}', 101 | 'sample/controllers/*.{css,js,html}', 102 | 'sample/views/*.{css,js,html}', 103 | 'src/*.js' 104 | ] 105 | } 106 | }, 107 | copy: { 108 | breadcrumb: { 109 | files: [ 110 | { 111 | flatten: true, 112 | expand: true, 113 | src: [ 114 | 'src/angular-breadcrumb.js' 115 | ], 116 | dest: 'sample/asset/' 117 | } 118 | ] 119 | }, 120 | asset: { 121 | files: [ 122 | { 123 | flatten: true, 124 | expand: true, 125 | src: [ 126 | 'dist/angular-breadcrumb.js', 127 | 'bower_components/angular/angular.js', 128 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 129 | 'bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js', 130 | 'bower_components/bootstrap/docs/assets/css/bootstrap.css', 131 | 'bower_components/underscore/underscore.js' 132 | ], 133 | dest: 'sample/asset/' 134 | } 135 | ] 136 | }, 137 | img: { 138 | files: [ 139 | { 140 | flatten: true, 141 | expand: true, 142 | src: [ 143 | 'bower_components/bootstrap.css/img/glyphicons-halflings.png' 144 | ], 145 | dest: 'sample/img/' 146 | } 147 | ] 148 | } 149 | }, 150 | connect: { 151 | options: { 152 | port: 9000, 153 | hostname: 'localhost' 154 | }, 155 | livereload: { 156 | options: { 157 | middleware: function (connect) { 158 | return [ 159 | lrSnippet, 160 | mountFolder(connect, 'sample') 161 | ]; 162 | } 163 | } 164 | } 165 | }, 166 | open: { 167 | server: { 168 | url: 'http://localhost:<%= connect.options.port %>/index.html' 169 | } 170 | }, 171 | bump: { 172 | options: { 173 | files: ['package.json', 'bower.json'], 174 | updateConfigs: ['pkg'] 175 | } 176 | }, 177 | clean: { 178 | release: ["sample/*.zip"], 179 | test: ["testDependencies/*"], 180 | meteor: ['.build.*', 'versions.json'] 181 | }, 182 | exec: { 183 | 'meteor-init': { 184 | command: [ 185 | 'type meteor >/dev/null 2>&1 || { curl https://install.meteor.com/ | sh; }' 186 | ].join(';') 187 | }, 188 | 'meteor-publish': { 189 | command: 'meteor publish' 190 | } 191 | }, 192 | compress: { 193 | release: { 194 | options: { 195 | archive: 'sample/<%= pkg.name %>-<%= pkg.version %>.zip' 196 | }, 197 | files: [ 198 | { 199 | expand: true, 200 | cwd: 'release/', 201 | src: ['*.js'] 202 | } 203 | ] 204 | } 205 | }, 206 | replace: { 207 | release: { 208 | src: ['sample/views/home.html'], 209 | overwrite: true, 210 | replacements: [{ 211 | from: /angular-breadcrumb-[0-9]+\.[0-9]+\.[0-9]+\.zip/g, 212 | to: "angular-breadcrumb-<%= pkg.version %>.zip" 213 | }, 214 | { 215 | from: /\([0-9]+\.[0-9]+\.[0-9]+\)/g, 216 | to: "(<%= pkg.version %>)" 217 | }] 218 | } 219 | }, 220 | shell: { 221 | testMinimal: { 222 | command: 'bower install angular#=1.2.29 angular-mocks#=1.2.29 angular-sanitize#=1.2.29 angular-ui-router#=0.2.18 --config.directory=. --config.cwd=testDependencies' 223 | }, 224 | test1dot3: { 225 | command: 'bower install angular#=1.3.20 angular-mocks#=1.3.20 angular-sanitize#=1.3.20 angular-ui-router#=0.2.18 --config.directory=. --config.cwd=testDependencies' 226 | }, 227 | test1dot4: { 228 | command: 'bower install angular#=1.4.10 angular-mocks#=1.4.10 angular-sanitize#=1.4.10 angular-ui-router#=0.2.18 --config.directory=. --config.cwd=testDependencies' 229 | }, 230 | testLatest: { 231 | command: 'bower install angular angular-mocks angular-sanitize angular-ui-router --config.directory=. --config.cwd=testDependencies' 232 | } 233 | } 234 | 235 | }); 236 | 237 | // These plugins provide necessary tasks. 238 | grunt.loadNpmTasks('grunt-bump'); 239 | grunt.loadNpmTasks('grunt-contrib-clean'); 240 | grunt.loadNpmTasks('grunt-contrib-compress'); 241 | grunt.loadNpmTasks('grunt-contrib-concat'); 242 | grunt.loadNpmTasks('grunt-contrib-uglify'); 243 | grunt.loadNpmTasks('grunt-contrib-jshint'); 244 | grunt.loadNpmTasks('grunt-contrib-watch'); 245 | grunt.loadNpmTasks('grunt-contrib-copy'); 246 | grunt.loadNpmTasks('grunt-contrib-connect'); 247 | grunt.loadNpmTasks('grunt-conventional-changelog'); 248 | grunt.loadNpmTasks('grunt-karma'); 249 | grunt.loadNpmTasks('grunt-open'); 250 | grunt.loadNpmTasks('grunt-shell'); 251 | grunt.loadNpmTasks('grunt-text-replace'); 252 | grunt.loadNpmTasks('grunt-exec'); 253 | 254 | grunt.registerTask('meteor-publish', ['exec:meteor-init', 'exec:meteor-publish']); 255 | 256 | grunt.registerTask('test', ['jshint', 'testMin', 'test1dot3', 'test1dot4', 'testLatest']); 257 | grunt.registerTask('testMin', ['clean:test', 'shell:testMinimal', 'karma']); 258 | grunt.registerTask('test1dot3', ['clean:test', 'shell:test1dot3', 'karma']); 259 | grunt.registerTask('test1dot4', ['clean:test', 'shell:test1dot4', 'karma']); 260 | grunt.registerTask('testLatest', ['clean:test', 'shell:testLatest', 'karma']); 261 | 262 | grunt.registerTask('default', ['test', 'concat:dev', 'uglify:dev']); 263 | 264 | grunt.registerTask('sample', ['concat:dev', 'copy:asset', 'copy:img', 'connect:livereload', 'open', 'watch']); 265 | 266 | grunt.registerTask('release-prepare', 'Update all files for a release', function (target) { 267 | if (!target) { 268 | target = 'patch'; 269 | } 270 | grunt.task.run( 271 | 'bump-only:' + target, // Version update 272 | 'test', // Tests 273 | 'concat:release', // Concat with release banner 274 | 'uglify:release', // Minify with release banner 275 | 'changelog', // Changelog update 276 | 'clean:release', // Delete old version download file 277 | 'compress:release', // New version download file 278 | 'replace:release' // Update version in download button (link & label) 279 | ); 280 | }); 281 | 282 | }; 283 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Nicolas Cuillery 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![angular-breadcrumb](https://raw.github.com/ncuillery/angular-breadcrumb/master/sample/img/logo/angular-breadcrumb-logo-400.png)](http://ncuillery.github.io/angular-breadcrumb/) 2 | [![Build Status](https://travis-ci.org/ncuillery/angular-breadcrumb.svg)](https://travis-ci.org/ncuillery/angular-breadcrumb) [![Coverage Status](https://coveralls.io/repos/ncuillery/angular-breadcrumb/badge.svg)](https://coveralls.io/r/ncuillery/angular-breadcrumb) 3 | [![npm version](https://badge.fury.io/js/angular-breadcrumb.svg)](https://badge.fury.io/js/angular-breadcrumb) 4 | 5 | Angular-breadcrumb is a module for [AngularJS](http://angularjs.org), which generates a breadcrumb for any page of your application. It is strongly based on the [ui-router](https://github.com/angular-ui/ui-router) framework and its hierarchical tree of states. 6 | 7 | ## Key features 8 | - Build a breadcrumb with a step for each state in the current state's hierarchy, 9 | - Display a human readable label for each step and allows angular binding in it, 10 | - Work with minimal configuration, 11 | - Allow custom template (default is a predefined [Bootstrap 3 breadcrumb template](http://getbootstrap.com/components/#breadcrumbs)). 12 | 13 | ## Documentation 14 | - [Getting started](https://github.com/ncuillery/angular-breadcrumb/wiki/Getting-started) 15 | - [API Reference](https://github.com/ncuillery/angular-breadcrumb/wiki/API-Reference) 16 | - [Templating](https://github.com/ncuillery/angular-breadcrumb/wiki/Templating) 17 | 18 | ## Sample 19 | See angular-breadcrumb in action [here](http://ncuillery.github.io/angular-breadcrumb/#/sample) 20 | 21 | ## Release History 22 | See [CHANGELOG.md](https://github.com/ncuillery/angular-breadcrumb/blob/master/CHANGELOG.md) 23 | 24 | ## Contributing 25 | See [CONTRIBUTING.md](https://github.com/ncuillery/angular-breadcrumb/blob/master/CONTRIBUTING.md) 26 | 27 | 28 | ## License 29 | Copyright (c) 2013 Nicolas Cuillery 30 | Licensed under the MIT license. 31 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-breadcrumb", 3 | "description": "AngularJS module that generates a breadcrumb from ui-router's states", 4 | "version": "0.5.0", 5 | "main": "release/angular-breadcrumb.js", 6 | "ignore": [ 7 | "sample", 8 | "src", 9 | "test", 10 | ".bowerrc", 11 | ".coveralls.yml", 12 | ".gitignore", 13 | ".jshintrc", 14 | ".travis.yml", 15 | "gruntfile.js", 16 | "bower.json", 17 | "karma.conf.js", 18 | "libpeerconnection.log", 19 | "package.json", 20 | "README.md" 21 | ], 22 | "dependencies": { 23 | "angular": ">=1.2.0", 24 | "angular-ui-router": ">=0.2.0" 25 | }, 26 | "devDependencies": { 27 | "bootstrap": "~2.3.2", 28 | "angular-ui-bootstrap-bower": "~0.8.0", 29 | "underscore": "~1.5.1", 30 | "angular-mocks": ">=1.0.8", 31 | "angular-sanitize": ">=1.0.8" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dist/angular-breadcrumb.js: -------------------------------------------------------------------------------- 1 | /*! angular-breadcrumb - v0.4.1-dev-2016-04-12 2 | * http://ncuillery.github.io/angular-breadcrumb 3 | * Copyright (c) 2016 Nicolas Cuillery; Licensed MIT */ 4 | 5 | (function (window, angular, undefined) { 6 | 'use strict'; 7 | 8 | function isAOlderThanB(scopeA, scopeB) { 9 | if(angular.equals(scopeA.length, scopeB.length)) { 10 | return scopeA > scopeB; 11 | } else { 12 | return scopeA.length > scopeB.length; 13 | } 14 | } 15 | 16 | function parseStateRef(ref) { 17 | var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); 18 | if (!parsed || parsed.length !== 4) { throw new Error("Invalid state ref '" + ref + "'"); } 19 | return { state: parsed[1], paramExpr: parsed[3] || null }; 20 | } 21 | 22 | var $registeredListeners = {}; 23 | function registerListenerOnce(tag, $rootScope, event, fn) { 24 | var deregisterListenerFn = $registeredListeners[tag]; 25 | if ( deregisterListenerFn !== undefined ) { 26 | deregisterListenerFn(); 27 | } 28 | deregisterListenerFn = $rootScope.$on(event, fn); 29 | $registeredListeners[tag] = deregisterListenerFn; 30 | } 31 | 32 | function $Breadcrumb() { 33 | 34 | var $$options = { 35 | prefixStateName: null, 36 | template: 'bootstrap3', 37 | templateUrl: null, 38 | templateLast: 'default', 39 | templateLastUrl: null, 40 | includeAbstract : false 41 | }; 42 | 43 | this.setOptions = function(options) { 44 | angular.extend($$options, options); 45 | }; 46 | 47 | this.$get = ['$state', '$stateParams', '$rootScope', function($state, $stateParams, $rootScope) { 48 | 49 | var $lastViewScope = $rootScope; 50 | 51 | // Early catch of $viewContentLoaded event 52 | registerListenerOnce('$Breadcrumb.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 53 | // With nested views, the event occur several times, in "wrong" order 54 | if(!event.targetScope.ncyBreadcrumbIgnore && 55 | isAOlderThanB(event.targetScope.$id, $lastViewScope.$id)) { 56 | $lastViewScope = event.targetScope; 57 | } 58 | }); 59 | 60 | // Get the parent state 61 | var $$parentState = function(state) { 62 | // Check if state has explicit parent OR we try guess parent from its name 63 | var parent = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1]; 64 | var isObjectParent = typeof parent === "object"; 65 | // if parent is a object reference, then extract the name 66 | return isObjectParent ? parent.name : parent; 67 | }; 68 | 69 | // Add the state in the chain if not already in and if not abstract 70 | var $$addStateInChain = function(chain, stateRef) { 71 | var conf, 72 | parentParams, 73 | ref = parseStateRef(stateRef), 74 | force = false, 75 | skip = false; 76 | 77 | for(var i=0, l=chain.length; i' + 216 | '
  • ' + 217 | '{{step.ncyBreadcrumbLabel}}' + 218 | '{{step.ncyBreadcrumbLabel}}' + 219 | '/' + 220 | '
  • ' + 221 | '', 222 | bootstrap3: '' 228 | }; 229 | 230 | return { 231 | restrict: 'AE', 232 | replace: true, 233 | scope: {}, 234 | template: $breadcrumb.getTemplate($$templates), 235 | templateUrl: $breadcrumb.getTemplateUrl(), 236 | link: { 237 | post: function postLink(scope) { 238 | var labelWatchers = []; 239 | 240 | var renderBreadcrumb = function() { 241 | deregisterWatchers(labelWatchers); 242 | labelWatchers = []; 243 | 244 | var viewScope = $breadcrumb.$getLastViewScope(); 245 | scope.steps = $breadcrumb.getStatesChain(); 246 | angular.forEach(scope.steps, function (step) { 247 | if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { 248 | var parseLabel = $interpolate(step.ncyBreadcrumb.label); 249 | step.ncyBreadcrumbLabel = parseLabel(viewScope); 250 | // Watcher for further viewScope updates 251 | registerWatchers(labelWatchers, parseLabel, viewScope, step); 252 | } else { 253 | step.ncyBreadcrumbLabel = step.name; 254 | } 255 | }); 256 | }; 257 | 258 | registerListenerOnce('BreadcrumbDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 259 | if(!event.targetScope.ncyBreadcrumbIgnore) { 260 | renderBreadcrumb(); 261 | } 262 | }); 263 | 264 | // View(s) may be already loaded while the directive's linking 265 | renderBreadcrumb(); 266 | } 267 | } 268 | }; 269 | } 270 | BreadcrumbDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 271 | 272 | function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { 273 | var $$templates = { 274 | 'default': '{{ncyBreadcrumbLabel}}' 275 | }; 276 | 277 | return { 278 | restrict: 'A', 279 | scope: {}, 280 | template: $breadcrumb.getTemplateLast($$templates), 281 | templateUrl: $breadcrumb.getTemplateLastUrl(), 282 | compile: function(cElement, cAttrs) { 283 | 284 | // Override the default template if ncyBreadcrumbLast has a value 285 | // This should likely be removed in a future version since global 286 | // templating is now available for ncyBreadcrumbLast 287 | var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbLast); 288 | if(template) { 289 | cElement.html(template); 290 | } 291 | 292 | return { 293 | post: function postLink(scope) { 294 | var labelWatchers = []; 295 | 296 | var renderLabel = function() { 297 | deregisterWatchers(labelWatchers); 298 | labelWatchers = []; 299 | 300 | var viewScope = $breadcrumb.$getLastViewScope(); 301 | var lastStep = $breadcrumb.getLastStep(); 302 | if(lastStep) { 303 | scope.ncyBreadcrumbLink = lastStep.ncyBreadcrumbLink; 304 | if (lastStep.ncyBreadcrumb && lastStep.ncyBreadcrumb.label) { 305 | var parseLabel = $interpolate(lastStep.ncyBreadcrumb.label); 306 | scope.ncyBreadcrumbLabel = parseLabel(viewScope); 307 | // Watcher for further viewScope updates 308 | // Tricky last arg: the last step is the entire scope of the directive ! 309 | registerWatchers(labelWatchers, parseLabel, viewScope, scope); 310 | } else { 311 | scope.ncyBreadcrumbLabel = lastStep.name; 312 | } 313 | } 314 | }; 315 | 316 | registerListenerOnce('BreadcrumbLastDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 317 | if(!event.targetScope.ncyBreadcrumbIgnore) { 318 | renderLabel(); 319 | } 320 | }); 321 | 322 | // View(s) may be already loaded while the directive's linking 323 | renderLabel(); 324 | } 325 | }; 326 | 327 | } 328 | }; 329 | } 330 | BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 331 | 332 | function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { 333 | 334 | return { 335 | restrict: 'A', 336 | scope: {}, 337 | template: '{{ncyBreadcrumbChain}}', 338 | 339 | compile: function(cElement, cAttrs) { 340 | // Override the default template if ncyBreadcrumbText has a value 341 | var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); 342 | if(template) { 343 | cElement.html(template); 344 | } 345 | 346 | var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator) || ' / '; 347 | 348 | return { 349 | post: function postLink(scope) { 350 | var labelWatchers = []; 351 | 352 | var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope) { 353 | angular.forEach(getExpression(interpolationFunction), function(expression) { 354 | var watcher = viewScope.$watch(expression, function(newValue, oldValue) { 355 | if (newValue !== oldValue) { 356 | renderLabel(); 357 | } 358 | }); 359 | labelWatcherArray.push(watcher); 360 | }); 361 | }; 362 | 363 | var renderLabel = function() { 364 | deregisterWatchers(labelWatchers); 365 | labelWatchers = []; 366 | 367 | var viewScope = $breadcrumb.$getLastViewScope(); 368 | var steps = $breadcrumb.getStatesChain(); 369 | var combinedLabels = []; 370 | angular.forEach(steps, function (step) { 371 | if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { 372 | var parseLabel = $interpolate(step.ncyBreadcrumb.label); 373 | combinedLabels.push(parseLabel(viewScope)); 374 | // Watcher for further viewScope updates 375 | registerWatchersText(labelWatchers, parseLabel, viewScope); 376 | } else { 377 | combinedLabels.push(step.name); 378 | } 379 | }); 380 | 381 | scope.ncyBreadcrumbChain = combinedLabels.join(separator); 382 | }; 383 | 384 | registerListenerOnce('BreadcrumbTextDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 385 | if(!event.targetScope.ncyBreadcrumbIgnore) { 386 | renderLabel(); 387 | } 388 | }); 389 | 390 | // View(s) may be already loaded while the directive's linking 391 | renderLabel(); 392 | } 393 | }; 394 | 395 | } 396 | }; 397 | } 398 | BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 399 | 400 | angular.module('ncy-angular-breadcrumb', ['ui.router.state']) 401 | .provider('$breadcrumb', $Breadcrumb) 402 | .directive('ncyBreadcrumb', BreadcrumbDirective) 403 | .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) 404 | .directive('ncyBreadcrumbText', BreadcrumbTextDirective); 405 | })(window, window.angular); 406 | -------------------------------------------------------------------------------- /dist/angular-breadcrumb.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-breadcrumb - v0.4.1-dev-2016-04-12 2 | * http://ncuillery.github.io/angular-breadcrumb 3 | * Copyright (c) 2016 Nicolas Cuillery; Licensed MIT */ 4 | !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(a,b,d,e){var f=k[a];f!==c&&f(),f=b.$on(d,e),k[a]=f}function g(){var a={prefixStateName:null,template:"bootstrap3",templateUrl:null,templateLast:"default",templateLastUrl:null,includeAbstract:!1};this.setOptions=function(c){b.extend(a,c)},this.$get=["$state","$stateParams","$rootScope",function(b,g,h){var i=h;f("$Breadcrumb.$viewContentLoaded",h,"$viewContentLoaded",function(a){!a.targetScope.ncyBreadcrumbIgnore&&d(a.targetScope.$id,i.$id)&&(i=a.targetScope)});var j=function(a){var b=a.parent||(/^(.+)\.[^.]+$/.exec(a.name)||[])[1],c="object"==typeof b;return c?b.name:b},k=function(c,d){for(var f,h,j=e(d),k=!1,l=!1,m=0,n=c.length;n>m;m+=1)if(c[m].name===j.state)return;f=b.get(j.state),f.ncyBreadcrumb&&(f.ncyBreadcrumb.force&&(k=!0),f.ncyBreadcrumb.skip&&(l=!0)),f["abstract"]&&!a.includeAbstract&&!k||l||(j.paramExpr&&(h=i.$eval(j.paramExpr)),f.ncyBreadcrumbLink=b.href(j.state,h||g||{}),f.ncyBreadcrumbStateRef=d,c.unshift(f))},l=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(i):d.ncyBreadcrumb.parent;if(g)return g}return j(d)};return{getTemplate:function(b){return a.templateUrl?null:b[a.template]?b[a.template]:a.template},getTemplateUrl:function(){return a.templateUrl},getTemplateLast:function(b){return a.templateLastUrl?null:b[a.templateLast]?b[a.templateLast]:a.templateLast},getTemplateLastUrl:function(){return a.templateLastUrl},getStatesChain:function(c){for(var d=[],e=b.$current.self.name;e;e=l(e))if(k(d,e),c&&d.length)return d;return a.prefixStateName&&k(d,a.prefixStateName),d},getLastStep:function(){var a=this.getStatesChain(!0);return a.length?a[0]:c},$getLastViewScope:function(){return i}}}]}function h(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 g=[],h=function(){n(g),g=[];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),m(g,c,d,b)}else b.ncyBreadcrumbLabel=b.name})};f("BreadcrumbDirective.$viewContentLoaded",d,"$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||h()}),h()}}}}function i(a,b,c){var d={"default":"{{ncyBreadcrumbLabel}}"};return{restrict:"A",scope:{},template:b.getTemplateLast(d),templateUrl:b.getTemplateLastUrl(),compile:function(d,e){var g=d.attr(e.$attr.ncyBreadcrumbLast);return g&&d.html(g),{post:function(d){var e=[],g=function(){n(e),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),m(e,g,c,d)}else d.ncyBreadcrumbLabel=f.name};f("BreadcrumbLastDirective.$viewContentLoaded",c,"$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||g()}),g()}}}}}function j(a,c,d){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbChain}}",compile:function(e,g){var h=e.attr(g.$attr.ncyBreadcrumbText);h&&e.html(h);var i=e.attr(g.$attr.ncyBreadcrumbTextSeparator)||" / ";return{post:function(e){var g=[],h=function(a,c,d){b.forEach(l(c),function(b){var c=d.$watch(b,function(a,b){a!==b&&j()});a.push(c)})},j=function(){n(g),g=[];var d=c.$getLastViewScope(),f=c.getStatesChain(),j=[];b.forEach(f,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);j.push(c(d)),h(g,c,d)}else j.push(b.name)}),e.ncyBreadcrumbChain=j.join(i)};f("BreadcrumbTextDirective.$viewContentLoaded",d,"$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||j()}),j()}}}}}var k={},l=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},m=function(a,c,d,e){b.forEach(l(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d)});a.push(f)})},n=function(a){b.forEach(a,function(a){a()})};h.$inject=["$interpolate","$breadcrumb","$rootScope"],i.$inject=["$interpolate","$breadcrumb","$rootScope"],j.$inject=["$interpolate","$breadcrumb","$rootScope"],b.module("ncy-angular-breadcrumb",["ui.router.state"]).provider("$breadcrumb",g).directive("ncyBreadcrumb",h).directive("ncyBreadcrumbLast",i).directive("ncyBreadcrumbText",j)}(window,window.angular); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'testDependencies/angular/angular.js', 15 | 'testDependencies/angular-mocks/angular-mocks.js', 16 | 'testDependencies/angular-sanitize/angular-sanitize.js', 17 | 'testDependencies/angular-ui-router/release/angular-ui-router.js', 18 | 'bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js', 19 | 'bower_components/underscore/underscore.js', 20 | 'src/angular-breadcrumb.js', 21 | 'sample/app.js', 22 | 'sample/controllers/room_detail.js', 23 | 'test/mock/**/*.js', 24 | 'test/spec/**/*.js' 25 | ], 26 | 27 | preprocessors: { 28 | 'src/angular-breadcrumb.js': 'coverage' 29 | }, 30 | 31 | reporters: ['story', 'coverage'], 32 | 33 | coverageReporter: { 34 | type : 'lcov', 35 | dir : 'coverage/' 36 | }, 37 | 38 | // list of files / patterns to exclude 39 | exclude: [], 40 | 41 | // web server port 42 | port: 8080, 43 | 44 | // level of logging 45 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 46 | logLevel: config.LOG_INFO, 47 | 48 | 49 | // enable / disable watching file and executing tests whenever any file changes 50 | autoWatch: false, 51 | 52 | 53 | // Start these browsers, currently available: 54 | // - Chrome 55 | // - ChromeCanary 56 | // - Firefox 57 | // - Opera 58 | // - Safari (only Mac) 59 | // - PhantomJS 60 | // - IE (only Windows) 61 | browsers: ['PhantomJS'], 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, it capture browsers, run tests and exit 66 | singleRun: true 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ncuillery:angular-breadcrumb', 3 | version: '0.3.3', 4 | summary: 'angular-breadcrumb for meteor!', 5 | git: 'https://github.com/ncuillery/angular-breadcrumb', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function (api) { 10 | api.versionsFrom('METEOR@0.9.0.1'); 11 | api.use('urigo:angular@0.8.4', 'client'); 12 | api.addFiles('dist/angular-breadcrumb.js', 'client'); 13 | }); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-breadcrumb", 3 | "description": "AngularJS module that generates a breadcrumb from ui-router's states", 4 | "version": "0.5.0", 5 | "homepage": "http://ncuillery.github.io/angular-breadcrumb", 6 | "author": { 7 | "name": "Nicolas Cuillery", 8 | "email": "nicolas.cuillery@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/ncuillery/angular-breadcrumb.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/ncuillery/angular-breadcrumb/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/ncuillery/angular-breadcrumb/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "main": "release/angular-breadcrumb.js", 24 | "engines": { 25 | "node": ">= 0.10.0" 26 | }, 27 | "scripts": { 28 | "test": "grunt test" 29 | }, 30 | "devDependencies": { 31 | "connect-livereload": "~0.2.0", 32 | "coveralls": "~2.3.0", 33 | "grunt": "~0.4.1", 34 | "grunt-bump": "0.0.14", 35 | "grunt-contrib-clean": "^0.5.0", 36 | "grunt-contrib-compress": "^0.9.1", 37 | "grunt-contrib-concat": "~0.3.0", 38 | "grunt-contrib-connect": "~0.3.0", 39 | "grunt-contrib-copy": "~0.4.1", 40 | "grunt-contrib-jshint": "~0.6.0", 41 | "grunt-contrib-uglify": "~0.2.0", 42 | "grunt-contrib-watch": "~0.4.0", 43 | "grunt-conventional-changelog": "~1.1.0", 44 | "grunt-exec": "^0.4.6", 45 | "grunt-karma": "~0.12.2", 46 | "grunt-open": "~0.2.0", 47 | "grunt-shell": "~0.7.0", 48 | "grunt-text-replace": "^0.3.12", 49 | "jasmine-core": "^2.4.1", 50 | "karma": "^0.13.22", 51 | "karma-coverage": "~0.1.5", 52 | "karma-jasmine": "^0.3.8", 53 | "karma-phantomjs-launcher": "^1.0.0", 54 | "karma-story-reporter": "^0.3.1", 55 | "phantomjs-prebuilt": "^2.1.7", 56 | "spacejam": "^1.1.1" 57 | }, 58 | "keywords": [ 59 | "angular", 60 | "breadcrumb" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /release/angular-breadcrumb.js: -------------------------------------------------------------------------------- 1 | /*! angular-breadcrumb - v0.5.0 2 | * http://ncuillery.github.io/angular-breadcrumb 3 | * Copyright (c) 2016 Nicolas Cuillery; Licensed MIT */ 4 | 5 | (function (window, angular, undefined) { 6 | 'use strict'; 7 | 8 | function isAOlderThanB(scopeA, scopeB) { 9 | if(angular.equals(scopeA.length, scopeB.length)) { 10 | return scopeA > scopeB; 11 | } else { 12 | return scopeA.length > scopeB.length; 13 | } 14 | } 15 | 16 | function parseStateRef(ref) { 17 | var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); 18 | if (!parsed || parsed.length !== 4) { throw new Error("Invalid state ref '" + ref + "'"); } 19 | return { state: parsed[1], paramExpr: parsed[3] || null }; 20 | } 21 | 22 | var $registeredListeners = {}; 23 | function registerListenerOnce(tag, $rootScope, event, fn) { 24 | var deregisterListenerFn = $registeredListeners[tag]; 25 | if ( deregisterListenerFn !== undefined ) { 26 | deregisterListenerFn(); 27 | } 28 | deregisterListenerFn = $rootScope.$on(event, fn); 29 | $registeredListeners[tag] = deregisterListenerFn; 30 | } 31 | 32 | function $Breadcrumb() { 33 | 34 | var $$options = { 35 | prefixStateName: null, 36 | template: 'bootstrap3', 37 | templateUrl: null, 38 | templateLast: 'default', 39 | templateLastUrl: null, 40 | includeAbstract : false 41 | }; 42 | 43 | this.setOptions = function(options) { 44 | angular.extend($$options, options); 45 | }; 46 | 47 | this.$get = ['$state', '$stateParams', '$rootScope', function($state, $stateParams, $rootScope) { 48 | 49 | var $lastViewScope = $rootScope; 50 | 51 | // Early catch of $viewContentLoaded event 52 | registerListenerOnce('$Breadcrumb.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 53 | // With nested views, the event occur several times, in "wrong" order 54 | if(!event.targetScope.ncyBreadcrumbIgnore && 55 | isAOlderThanB(event.targetScope.$id, $lastViewScope.$id)) { 56 | $lastViewScope = event.targetScope; 57 | } 58 | }); 59 | 60 | // Get the parent state 61 | var $$parentState = function(state) { 62 | // Check if state has explicit parent OR we try guess parent from its name 63 | var parent = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1]; 64 | var isObjectParent = typeof parent === "object"; 65 | // if parent is a object reference, then extract the name 66 | return isObjectParent ? parent.name : parent; 67 | }; 68 | 69 | // Add the state in the chain if not already in and if not abstract 70 | var $$addStateInChain = function(chain, stateRef) { 71 | var conf, 72 | parentParams, 73 | ref = parseStateRef(stateRef), 74 | force = false, 75 | skip = false; 76 | 77 | for(var i=0, l=chain.length; i' + 216 | '
  • ' + 217 | '{{step.ncyBreadcrumbLabel}}' + 218 | '{{step.ncyBreadcrumbLabel}}' + 219 | '/' + 220 | '
  • ' + 221 | '', 222 | bootstrap3: '' 228 | }; 229 | 230 | return { 231 | restrict: 'AE', 232 | replace: true, 233 | scope: {}, 234 | template: $breadcrumb.getTemplate($$templates), 235 | templateUrl: $breadcrumb.getTemplateUrl(), 236 | link: { 237 | post: function postLink(scope) { 238 | var labelWatchers = []; 239 | 240 | var renderBreadcrumb = function() { 241 | deregisterWatchers(labelWatchers); 242 | labelWatchers = []; 243 | 244 | var viewScope = $breadcrumb.$getLastViewScope(); 245 | scope.steps = $breadcrumb.getStatesChain(); 246 | angular.forEach(scope.steps, function (step) { 247 | if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { 248 | var parseLabel = $interpolate(step.ncyBreadcrumb.label); 249 | step.ncyBreadcrumbLabel = parseLabel(viewScope); 250 | // Watcher for further viewScope updates 251 | registerWatchers(labelWatchers, parseLabel, viewScope, step); 252 | } else { 253 | step.ncyBreadcrumbLabel = step.name; 254 | } 255 | }); 256 | }; 257 | 258 | registerListenerOnce('BreadcrumbDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 259 | if(!event.targetScope.ncyBreadcrumbIgnore) { 260 | renderBreadcrumb(); 261 | } 262 | }); 263 | 264 | // View(s) may be already loaded while the directive's linking 265 | renderBreadcrumb(); 266 | } 267 | } 268 | }; 269 | } 270 | BreadcrumbDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 271 | 272 | function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { 273 | var $$templates = { 274 | 'default': '{{ncyBreadcrumbLabel}}' 275 | }; 276 | 277 | return { 278 | restrict: 'A', 279 | scope: {}, 280 | template: $breadcrumb.getTemplateLast($$templates), 281 | templateUrl: $breadcrumb.getTemplateLastUrl(), 282 | compile: function(cElement, cAttrs) { 283 | 284 | // Override the default template if ncyBreadcrumbLast has a value 285 | // This should likely be removed in a future version since global 286 | // templating is now available for ncyBreadcrumbLast 287 | var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbLast); 288 | if(template) { 289 | cElement.html(template); 290 | } 291 | 292 | return { 293 | post: function postLink(scope) { 294 | var labelWatchers = []; 295 | 296 | var renderLabel = function() { 297 | deregisterWatchers(labelWatchers); 298 | labelWatchers = []; 299 | 300 | var viewScope = $breadcrumb.$getLastViewScope(); 301 | var lastStep = $breadcrumb.getLastStep(); 302 | if(lastStep) { 303 | scope.ncyBreadcrumbLink = lastStep.ncyBreadcrumbLink; 304 | if (lastStep.ncyBreadcrumb && lastStep.ncyBreadcrumb.label) { 305 | var parseLabel = $interpolate(lastStep.ncyBreadcrumb.label); 306 | scope.ncyBreadcrumbLabel = parseLabel(viewScope); 307 | // Watcher for further viewScope updates 308 | // Tricky last arg: the last step is the entire scope of the directive ! 309 | registerWatchers(labelWatchers, parseLabel, viewScope, scope); 310 | } else { 311 | scope.ncyBreadcrumbLabel = lastStep.name; 312 | } 313 | } 314 | }; 315 | 316 | registerListenerOnce('BreadcrumbLastDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 317 | if(!event.targetScope.ncyBreadcrumbIgnore) { 318 | renderLabel(); 319 | } 320 | }); 321 | 322 | // View(s) may be already loaded while the directive's linking 323 | renderLabel(); 324 | } 325 | }; 326 | 327 | } 328 | }; 329 | } 330 | BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 331 | 332 | function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { 333 | 334 | return { 335 | restrict: 'A', 336 | scope: {}, 337 | template: '{{ncyBreadcrumbChain}}', 338 | 339 | compile: function(cElement, cAttrs) { 340 | // Override the default template if ncyBreadcrumbText has a value 341 | var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); 342 | if(template) { 343 | cElement.html(template); 344 | } 345 | 346 | var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator) || ' / '; 347 | 348 | return { 349 | post: function postLink(scope) { 350 | var labelWatchers = []; 351 | 352 | var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope) { 353 | angular.forEach(getExpression(interpolationFunction), function(expression) { 354 | var watcher = viewScope.$watch(expression, function(newValue, oldValue) { 355 | if (newValue !== oldValue) { 356 | renderLabel(); 357 | } 358 | }); 359 | labelWatcherArray.push(watcher); 360 | }); 361 | }; 362 | 363 | var renderLabel = function() { 364 | deregisterWatchers(labelWatchers); 365 | labelWatchers = []; 366 | 367 | var viewScope = $breadcrumb.$getLastViewScope(); 368 | var steps = $breadcrumb.getStatesChain(); 369 | var combinedLabels = []; 370 | angular.forEach(steps, function (step) { 371 | if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { 372 | var parseLabel = $interpolate(step.ncyBreadcrumb.label); 373 | combinedLabels.push(parseLabel(viewScope)); 374 | // Watcher for further viewScope updates 375 | registerWatchersText(labelWatchers, parseLabel, viewScope); 376 | } else { 377 | combinedLabels.push(step.name); 378 | } 379 | }); 380 | 381 | scope.ncyBreadcrumbChain = combinedLabels.join(separator); 382 | }; 383 | 384 | registerListenerOnce('BreadcrumbTextDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 385 | if(!event.targetScope.ncyBreadcrumbIgnore) { 386 | renderLabel(); 387 | } 388 | }); 389 | 390 | // View(s) may be already loaded while the directive's linking 391 | renderLabel(); 392 | } 393 | }; 394 | 395 | } 396 | }; 397 | } 398 | BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 399 | 400 | angular.module('ncy-angular-breadcrumb', ['ui.router.state']) 401 | .provider('$breadcrumb', $Breadcrumb) 402 | .directive('ncyBreadcrumb', BreadcrumbDirective) 403 | .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) 404 | .directive('ncyBreadcrumbText', BreadcrumbTextDirective); 405 | })(window, window.angular); 406 | -------------------------------------------------------------------------------- /release/angular-breadcrumb.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-breadcrumb - v0.5.0 2 | * http://ncuillery.github.io/angular-breadcrumb 3 | * Copyright (c) 2016 Nicolas Cuillery; Licensed MIT */ 4 | !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(a,b,d,e){var f=k[a];f!==c&&f(),f=b.$on(d,e),k[a]=f}function g(){var a={prefixStateName:null,template:"bootstrap3",templateUrl:null,templateLast:"default",templateLastUrl:null,includeAbstract:!1};this.setOptions=function(c){b.extend(a,c)},this.$get=["$state","$stateParams","$rootScope",function(b,g,h){var i=h;f("$Breadcrumb.$viewContentLoaded",h,"$viewContentLoaded",function(a){!a.targetScope.ncyBreadcrumbIgnore&&d(a.targetScope.$id,i.$id)&&(i=a.targetScope)});var j=function(a){var b=a.parent||(/^(.+)\.[^.]+$/.exec(a.name)||[])[1],c="object"==typeof b;return c?b.name:b},k=function(c,d){for(var f,h,j=e(d),k=!1,l=!1,m=0,n=c.length;n>m;m+=1)if(c[m].name===j.state)return;f=b.get(j.state),f.ncyBreadcrumb&&(f.ncyBreadcrumb.force&&(k=!0),f.ncyBreadcrumb.skip&&(l=!0)),f["abstract"]&&!a.includeAbstract&&!k||l||(j.paramExpr&&(h=i.$eval(j.paramExpr)),f.ncyBreadcrumbLink=b.href(j.state,h||g||{}),f.ncyBreadcrumbStateRef=d,c.unshift(f))},l=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(i):d.ncyBreadcrumb.parent;if(g)return g}return j(d)};return{getTemplate:function(b){return a.templateUrl?null:b[a.template]?b[a.template]:a.template},getTemplateUrl:function(){return a.templateUrl},getTemplateLast:function(b){return a.templateLastUrl?null:b[a.templateLast]?b[a.templateLast]:a.templateLast},getTemplateLastUrl:function(){return a.templateLastUrl},getStatesChain:function(c){for(var d=[],e=b.$current.self.name;e;e=l(e))if(k(d,e),c&&d.length)return d;return a.prefixStateName&&k(d,a.prefixStateName),d},getLastStep:function(){var a=this.getStatesChain(!0);return a.length?a[0]:c},$getLastViewScope:function(){return i}}}]}function h(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 g=[],h=function(){n(g),g=[];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),m(g,c,d,b)}else b.ncyBreadcrumbLabel=b.name})};f("BreadcrumbDirective.$viewContentLoaded",d,"$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||h()}),h()}}}}function i(a,b,c){var d={"default":"{{ncyBreadcrumbLabel}}"};return{restrict:"A",scope:{},template:b.getTemplateLast(d),templateUrl:b.getTemplateLastUrl(),compile:function(d,e){var g=d.attr(e.$attr.ncyBreadcrumbLast);return g&&d.html(g),{post:function(d){var e=[],g=function(){n(e),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),m(e,g,c,d)}else d.ncyBreadcrumbLabel=f.name};f("BreadcrumbLastDirective.$viewContentLoaded",c,"$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||g()}),g()}}}}}function j(a,c,d){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbChain}}",compile:function(e,g){var h=e.attr(g.$attr.ncyBreadcrumbText);h&&e.html(h);var i=e.attr(g.$attr.ncyBreadcrumbTextSeparator)||" / ";return{post:function(e){var g=[],h=function(a,c,d){b.forEach(l(c),function(b){var c=d.$watch(b,function(a,b){a!==b&&j()});a.push(c)})},j=function(){n(g),g=[];var d=c.$getLastViewScope(),f=c.getStatesChain(),j=[];b.forEach(f,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);j.push(c(d)),h(g,c,d)}else j.push(b.name)}),e.ncyBreadcrumbChain=j.join(i)};f("BreadcrumbTextDirective.$viewContentLoaded",d,"$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||j()}),j()}}}}}var k={},l=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},m=function(a,c,d,e){b.forEach(l(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d)});a.push(f)})},n=function(a){b.forEach(a,function(a){a()})};h.$inject=["$interpolate","$breadcrumb","$rootScope"],i.$inject=["$interpolate","$breadcrumb","$rootScope"],j.$inject=["$interpolate","$breadcrumb","$rootScope"],b.module("ncy-angular-breadcrumb",["ui.router.state"]).provider("$breadcrumb",g).directive("ncyBreadcrumb",h).directive("ncyBreadcrumbLast",i).directive("ncyBreadcrumbText",j)}(window,window.angular); -------------------------------------------------------------------------------- /sample/angular-breadcrumb-0.5.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/angular-breadcrumb-0.5.0.zip -------------------------------------------------------------------------------- /sample/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | angular.module('ncy-sample', ['ui.router.state', 'ui.bootstrap', 'ncy-angular-breadcrumb']) 4 | .config(function($breadcrumbProvider) { 5 | $breadcrumbProvider.setOptions({ 6 | prefixStateName: 'home', 7 | template: 'bootstrap2' 8 | }); 9 | }) 10 | .config(function($stateProvider, $urlRouterProvider) { 11 | $stateProvider 12 | .state('home', { 13 | url: '/home', 14 | templateUrl: 'views/home.html', 15 | ncyBreadcrumb: { 16 | label: 'Home' 17 | } 18 | }) 19 | .state('sample', { 20 | url: '/sample', 21 | templateUrl: 'views/sample.html', 22 | ncyBreadcrumb: { 23 | label: 'Sample' 24 | } 25 | }) 26 | .state('booking', { 27 | url: '/booking', 28 | templateUrl: 'views/booking_list.html', 29 | controller: 'BookingListCtrl', 30 | ncyBreadcrumb: { 31 | label: 'Reservations', 32 | parent: 'sample' 33 | } 34 | }) 35 | .state('booking.day', { 36 | url: '/:year-:month-:day', 37 | templateUrl: 'views/booking_day.html', 38 | controller: 'BookingDayCtrl', 39 | onExit: function($rootScope) { 40 | $rootScope.reservationDate = undefined; 41 | }, 42 | ncyBreadcrumb: { 43 | label: 'Reservations for {{reservationDate | date:\'mediumDate\'}}' 44 | } 45 | }) 46 | .state('booking.day.detail', { 47 | url: '/{reservationId}', 48 | onEnter: function($stateParams, $state, $modal) { 49 | $modal.open({ 50 | templateUrl: "views/booking_detail.html", 51 | controller: 'BookingDetailCtrl' 52 | }).result.then(function(result) { 53 | return $state.go("^"); 54 | }, function(result) { 55 | return $state.go("^"); 56 | }); 57 | }, 58 | ncyBreadcrumb: { 59 | skip: true 60 | } 61 | }) 62 | .state('room', { 63 | url: '/room', 64 | templateUrl: 'views/room_list.html', 65 | controller: 'RoomListCtrl', 66 | ncyBreadcrumb: { 67 | label: 'Rooms', 68 | parent: 'sample' 69 | } 70 | }) 71 | .state('room.new', { 72 | url: '/new', 73 | views: { 74 | "@" : { 75 | templateUrl: 'views/room_form.html', 76 | controller: 'RoomDetailCtrl' 77 | } 78 | }, 79 | ncyBreadcrumb: { 80 | label: 'New room' 81 | } 82 | }) 83 | .state('room.detail', { 84 | url: '/{roomId}?from', 85 | views: { 86 | "@" : { 87 | templateUrl: 'views/room_detail.html', 88 | controller: 'RoomDetailCtrl' 89 | } 90 | }, 91 | ncyBreadcrumb: { 92 | label: 'Room {{room.roomNumber}}', 93 | parent: function ($scope) { 94 | return $scope.from || 'room'; 95 | } 96 | } 97 | }) 98 | .state('room.detail.edit', { 99 | url: '/edit', 100 | views: { 101 | "@" : { 102 | templateUrl: 'views/room_form.html', 103 | controller: 'RoomDetailCtrl' 104 | } 105 | }, 106 | ncyBreadcrumb: { 107 | label: 'Editing' 108 | } 109 | }); 110 | 111 | $urlRouterProvider.otherwise('/home'); 112 | 113 | }) 114 | .value('rooms', [ 115 | {roomId: 1, roomNumber: 101, type: 'Double'}, 116 | {roomId: 2, roomNumber: 102, type: 'Double'}, 117 | {roomId: 3, roomNumber: 103, type: 'Single'}, 118 | {roomId: 4, roomNumber: 104, type: 'Double'} 119 | ]) 120 | .factory('reservations', function(dateUtils) { 121 | return [ 122 | {reservationId: 1, guestName: 'Robert Smith', roomId: '2', from: dateUtils.addDays(-1), nights: 3}, 123 | {reservationId: 2, guestName: 'John Doe', roomId: '3', from: dateUtils.addDays(-8), nights: 5}, 124 | {reservationId: 3, guestName: 'William Gordon', roomId: '1', from: dateUtils.addDays(3), nights: 6}, 125 | {reservationId: 4, guestName: 'Michael Robinson', roomId: '2', from: dateUtils.addDays(6), nights: 2}, 126 | {reservationId: 5, guestName: 'Tracy Marschall', roomId: '3', from: dateUtils.addDays(12), nights: 1} 127 | ]; 128 | }) 129 | .factory('dateUtils', function() { 130 | return { 131 | addDays: function(days, date) { 132 | if(!date) { 133 | var todayTime = new Date(); 134 | todayTime.setHours(0,0,0,0); 135 | date = new Date(todayTime); 136 | } 137 | 138 | var newDate = new Date(date); 139 | newDate.setDate(date.getDate() + days); 140 | return newDate; 141 | } 142 | } 143 | }) 144 | .run(function($rootScope, $state, $breadcrumb) { 145 | $rootScope.isActive = function(stateName) { 146 | return $state.includes(stateName); 147 | } 148 | }); 149 | -------------------------------------------------------------------------------- /sample/controllers/booking_day.js: -------------------------------------------------------------------------------- 1 | angular.module('ncy-sample') 2 | .controller('BookingDayCtrl', function($scope, $rootScope, $state, $stateParams, rooms) { 3 | $rootScope.reservationDate = new Date($stateParams.year, $stateParams.month - 1, $stateParams.day); 4 | 5 | if(!$scope.between($rootScope.reservationDate).length) { 6 | $state.go('^'); 7 | } 8 | 9 | $scope.getRoom = function(id) { 10 | return _.findWhere(rooms, {roomId: parseInt(id)}); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /sample/controllers/booking_detail.js: -------------------------------------------------------------------------------- 1 | angular.module('ncy-sample') 2 | .controller('BookingDetailCtrl', function($scope, $stateParams, dateUtils, reservations, rooms) { 3 | $scope.addDays = dateUtils.addDays; 4 | $scope.reservation = _.findWhere(reservations, {reservationId: parseInt($stateParams.reservationId)}); 5 | $scope.room = _.findWhere(rooms, {roomId: parseInt($scope.reservation.roomId)}); 6 | $scope.dismiss = function() { 7 | $scope.$dismiss(); 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /sample/controllers/booking_list.js: -------------------------------------------------------------------------------- 1 | angular.module('ncy-sample') 2 | .controller('BookingListCtrl', function($scope, $rootScope, $state, dateUtils, reservations) { 3 | 4 | // Some hardcoded data ; 5 | $scope.reservations = reservations; 6 | 7 | $scope.$watch('reservationDate', function(newValue, oldValue) { 8 | $scope.dpModel = $rootScope.reservationDate; 9 | }); 10 | 11 | 12 | $scope.$watch('dpModel', function(newValue, oldValue) { 13 | if(newValue && !angular.equals(newValue, oldValue)) { 14 | $state.go('booking.day', {year: newValue.getFullYear(), month: newValue.getMonth() + 1, day: newValue.getDate()}); 15 | } 16 | }); 17 | 18 | $scope.between = function(date) { 19 | return _.filter($scope.reservations, function(reservation) { 20 | var from = reservation.from; 21 | var to = dateUtils.addDays(reservation.nights, reservation.from); 22 | return from <= date && date < to; 23 | }); 24 | }; 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /sample/controllers/room_detail.js: -------------------------------------------------------------------------------- 1 | angular.module('ncy-sample') 2 | .controller('RoomDetailCtrl', function($scope, $state, $stateParams, rooms) { 3 | $scope.rooms = rooms; 4 | if($stateParams.from) { 5 | $scope.from = $stateParams.from.split('|')[0]; 6 | $scope.reservationDate = new Date(parseInt($stateParams.from.split('|')[1])); 7 | } 8 | 9 | if($stateParams.roomId) { 10 | $scope.room = _.findWhere(rooms, {roomId: parseInt($stateParams.roomId)}); 11 | if($scope.room) { 12 | $scope.model = angular.copy($scope.room); 13 | } else { 14 | $state.go('^'); 15 | } 16 | 17 | } 18 | 19 | $scope.save = function() { 20 | if($scope.model.roomId) { 21 | angular.extend($scope.room, $scope.model); 22 | } else { 23 | var ids = _.map(rooms, function(room) { return room.roomId; }); 24 | $scope.model.roomId = _.max(ids) + 1; 25 | rooms.push($scope.model); 26 | } 27 | $state.go('^'); 28 | } 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /sample/controllers/room_list.js: -------------------------------------------------------------------------------- 1 | angular.module('ncy-sample') 2 | .controller('RoomListCtrl', function($scope, rooms) { 3 | $scope.rooms = rooms; 4 | }); 5 | -------------------------------------------------------------------------------- /sample/css/sample.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding-top: 35px; 4 | padding-bottom: 20px; 5 | } 6 | 7 | .github-ribbon { 8 | position: fixed; 9 | top: 0; 10 | right: 0; 11 | border: 0; 12 | z-index: 1035; 13 | } 14 | 15 | .navbar-github-btn iframe { 16 | margin-top: 5px; 17 | margin-bottom: -5px; 18 | } 19 | 20 | .action { 21 | margin-top: 30px; 22 | text-align: center; 23 | } 24 | 25 | .action a + a { 26 | margin-left: 10px; 27 | } 28 | -------------------------------------------------------------------------------- /sample/img/GitHub-Mark-16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/GitHub-Mark-16px.png -------------------------------------------------------------------------------- /sample/img/GitHub-Mark-24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/GitHub-Mark-24px.png -------------------------------------------------------------------------------- /sample/img/download-24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/download-24px.png -------------------------------------------------------------------------------- /sample/img/logo/angular-breadcrumb-logo-1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/logo/angular-breadcrumb-logo-1200.png -------------------------------------------------------------------------------- /sample/img/logo/angular-breadcrumb-logo-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/logo/angular-breadcrumb-logo-200.png -------------------------------------------------------------------------------- /sample/img/logo/angular-breadcrumb-logo-3000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/logo/angular-breadcrumb-logo-3000.png -------------------------------------------------------------------------------- /sample/img/logo/angular-breadcrumb-logo-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/logo/angular-breadcrumb-logo-400.png -------------------------------------------------------------------------------- /sample/img/logo/angular-breadcrumb-logo-800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/logo/angular-breadcrumb-logo-800.png -------------------------------------------------------------------------------- /sample/img/logo/angular-breadcrumb-logo-orig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncuillery/angular-breadcrumb/301df6351f9272347ad3bd4b75f51a4d245dd2ef/sample/img/logo/angular-breadcrumb-logo-orig.png -------------------------------------------------------------------------------- /sample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 41 | 42 |
    43 |
    44 |
    45 | 46 |
    47 | 48 |
    49 |
    50 | 51 |
    52 |
    53 |
    54 | 55 | 56 | Fork me on GitHub 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /sample/views/booking_day.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    IdRoom
    {{reservation.reservationId}}{{getRoom(reservation.roomId).roomNumber}}View
    17 | -------------------------------------------------------------------------------- /sample/views/booking_detail.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |

    Reservation {{reservation.reservationId}}

    4 |
    5 |
    Id
    {{reservation.reservationId}}
    6 |
    Guest
    {{reservation.guestName}}
    7 |
    Room
    {{room.roomNumber}}
    8 |
    From
    {{reservation.from | date:'mediumDate'}}
    9 |
    To
    {{addDays(reservation.nights, reservation.from) | date:'mediumDate'}}
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /sample/views/booking_list.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Booking

    4 |
    5 |
    6 | 7 |
    8 |
    9 | 10 |
    11 |
    12 | 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /sample/views/home.html: -------------------------------------------------------------------------------- 1 |

    Description

    2 |
    3 |

    angular-breadcrumb is a module for AngularJS, which generates a breadcrumb for any page of your application. It is strongly based on the UI-router framework and its hierarchical tree of states.

    4 |

    Browse to the sample to see the example above working.

    5 |
    6 | 7 |

    Key features

    8 |
      9 |
    • Build a breadcrumb with a step for each state in the current state's hierarchy,
    • 10 |
    • Display a human readable label for each step and allows angular binding in it,
    • 11 |
    • Work with minimal configuration,
    • 12 |
    • Allow custom template (default is a predefined Bootstrap 3 breadcrumb template).
    • 13 |
    14 | 15 | -------------------------------------------------------------------------------- /sample/views/room_detail.html: -------------------------------------------------------------------------------- 1 |

    Room {{room.roomNumber}}

    2 |
    3 |
    Id
    {{room.roomId}}
    4 |
    Number
    {{room.roomNumber}}
    5 |
    Type
    {{room.type}}
    6 |
    7 |

    Edit

    8 | -------------------------------------------------------------------------------- /sample/views/room_form.html: -------------------------------------------------------------------------------- 1 |

    Edition room {{room.roomId}}

    2 |

    New room

    3 | 4 |
    5 | 6 |
    7 | 8 |
    9 | 10 |
    11 |
    12 | 13 |
    14 | 15 |
    16 | 17 |
    18 |
    19 | 20 |
    21 | 22 |
    23 | 24 |
    25 |
    26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /sample/views/room_list.html: -------------------------------------------------------------------------------- 1 |

    Rooms

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    IdNumberType
    {{item.roomId}}{{item.roomNumber}}{{item.type}}ViewEdit
    22 | 23 |

    New

    24 | -------------------------------------------------------------------------------- /sample/views/sample.html: -------------------------------------------------------------------------------- 1 |

    Sample

    2 |
    3 |

    This app is a basic information system for hotel. Its main purpose is to illustrate how work the 4 | module angular-breadcrumb in various situations.

    5 |

    Here is the state machine :

     6 | ─ home (defined as prefix in the breadcrumb)
     7 | ─ sample
     8 | ─ booking (override his parent state to 'sample' in the breadcrumb)
     9 |     └── booking.day (use the angular date filter in label)
    10 |           └── booking.day.detail (open a popup, not displayed in breadcrumb)
    11 | ─ room (override his parent state to 'sample' in the breadcrumb)
    12 |     └── room.detail (view included in root's view instead of parent's one, variable in breadcrumb label)
    13 |           └── room.detail.edit (allow to see the working parameterized link for parent 'room.detail')
    14 |

    15 |

    For more information, see the sample app's configuration.

    16 |
    17 |
    18 | Let's start navigating : 19 | Rooms 20 | Booking 21 |
    22 | -------------------------------------------------------------------------------- /src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "globals": { 15 | "angular": true 16 | } 17 | } -------------------------------------------------------------------------------- /src/angular-breadcrumb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isAOlderThanB(scopeA, scopeB) { 4 | if(angular.equals(scopeA.length, scopeB.length)) { 5 | return scopeA > scopeB; 6 | } else { 7 | return scopeA.length > scopeB.length; 8 | } 9 | } 10 | 11 | function parseStateRef(ref) { 12 | var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); 13 | if (!parsed || parsed.length !== 4) { throw new Error("Invalid state ref '" + ref + "'"); } 14 | return { state: parsed[1], paramExpr: parsed[3] || null }; 15 | } 16 | 17 | var $registeredListeners = {}; 18 | function registerListenerOnce(tag, $rootScope, event, fn) { 19 | var deregisterListenerFn = $registeredListeners[tag]; 20 | if ( deregisterListenerFn !== undefined ) { 21 | deregisterListenerFn(); 22 | } 23 | deregisterListenerFn = $rootScope.$on(event, fn); 24 | $registeredListeners[tag] = deregisterListenerFn; 25 | } 26 | 27 | function $Breadcrumb() { 28 | 29 | var $$options = { 30 | prefixStateName: null, 31 | template: 'bootstrap3', 32 | templateUrl: null, 33 | templateLast: 'default', 34 | templateLastUrl: null, 35 | includeAbstract : false 36 | }; 37 | 38 | this.setOptions = function(options) { 39 | angular.extend($$options, options); 40 | }; 41 | 42 | this.$get = ['$state', '$stateParams', '$rootScope', function($state, $stateParams, $rootScope) { 43 | 44 | var $lastViewScope = $rootScope; 45 | 46 | // Early catch of $viewContentLoaded event 47 | registerListenerOnce('$Breadcrumb.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 48 | // With nested views, the event occur several times, in "wrong" order 49 | if(!event.targetScope.ncyBreadcrumbIgnore && 50 | isAOlderThanB(event.targetScope.$id, $lastViewScope.$id)) { 51 | $lastViewScope = event.targetScope; 52 | } 53 | }); 54 | 55 | // Get the parent state 56 | var $$parentState = function(state) { 57 | // Check if state has explicit parent OR we try guess parent from its name 58 | var parent = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1]; 59 | var isObjectParent = typeof parent === "object"; 60 | // if parent is a object reference, then extract the name 61 | return isObjectParent ? parent.name : parent; 62 | }; 63 | 64 | // Add the state in the chain if not already in and if not abstract 65 | var $$addStateInChain = function(chain, stateRef) { 66 | var conf, 67 | parentParams, 68 | ref = parseStateRef(stateRef), 69 | force = false, 70 | skip = false; 71 | 72 | for(var i=0, l=chain.length; i' + 211 | '
  • ' + 212 | '{{step.ncyBreadcrumbLabel}}' + 213 | '{{step.ncyBreadcrumbLabel}}' + 214 | '/' + 215 | '
  • ' + 216 | '', 217 | bootstrap3: '' 223 | }; 224 | 225 | return { 226 | restrict: 'AE', 227 | replace: true, 228 | scope: {}, 229 | template: $breadcrumb.getTemplate($$templates), 230 | templateUrl: $breadcrumb.getTemplateUrl(), 231 | link: { 232 | post: function postLink(scope) { 233 | var labelWatchers = []; 234 | 235 | var renderBreadcrumb = function() { 236 | deregisterWatchers(labelWatchers); 237 | labelWatchers = []; 238 | 239 | var viewScope = $breadcrumb.$getLastViewScope(); 240 | scope.steps = $breadcrumb.getStatesChain(); 241 | angular.forEach(scope.steps, function (step) { 242 | if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { 243 | var parseLabel = $interpolate(step.ncyBreadcrumb.label); 244 | step.ncyBreadcrumbLabel = parseLabel(viewScope); 245 | // Watcher for further viewScope updates 246 | registerWatchers(labelWatchers, parseLabel, viewScope, step); 247 | } else { 248 | step.ncyBreadcrumbLabel = step.name; 249 | } 250 | }); 251 | }; 252 | 253 | registerListenerOnce('BreadcrumbDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 254 | if(!event.targetScope.ncyBreadcrumbIgnore) { 255 | renderBreadcrumb(); 256 | } 257 | }); 258 | 259 | // View(s) may be already loaded while the directive's linking 260 | renderBreadcrumb(); 261 | } 262 | } 263 | }; 264 | } 265 | BreadcrumbDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 266 | 267 | function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { 268 | var $$templates = { 269 | 'default': '{{ncyBreadcrumbLabel}}' 270 | }; 271 | 272 | return { 273 | restrict: 'A', 274 | scope: {}, 275 | template: $breadcrumb.getTemplateLast($$templates), 276 | templateUrl: $breadcrumb.getTemplateLastUrl(), 277 | compile: function(cElement, cAttrs) { 278 | 279 | // Override the default template if ncyBreadcrumbLast has a value 280 | // This should likely be removed in a future version since global 281 | // templating is now available for ncyBreadcrumbLast 282 | var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbLast); 283 | if(template) { 284 | cElement.html(template); 285 | } 286 | 287 | return { 288 | post: function postLink(scope) { 289 | var labelWatchers = []; 290 | 291 | var renderLabel = function() { 292 | deregisterWatchers(labelWatchers); 293 | labelWatchers = []; 294 | 295 | var viewScope = $breadcrumb.$getLastViewScope(); 296 | var lastStep = $breadcrumb.getLastStep(); 297 | if(lastStep) { 298 | scope.ncyBreadcrumbLink = lastStep.ncyBreadcrumbLink; 299 | if (lastStep.ncyBreadcrumb && lastStep.ncyBreadcrumb.label) { 300 | var parseLabel = $interpolate(lastStep.ncyBreadcrumb.label); 301 | scope.ncyBreadcrumbLabel = parseLabel(viewScope); 302 | // Watcher for further viewScope updates 303 | // Tricky last arg: the last step is the entire scope of the directive ! 304 | registerWatchers(labelWatchers, parseLabel, viewScope, scope); 305 | } else { 306 | scope.ncyBreadcrumbLabel = lastStep.name; 307 | } 308 | } 309 | }; 310 | 311 | registerListenerOnce('BreadcrumbLastDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 312 | if(!event.targetScope.ncyBreadcrumbIgnore) { 313 | renderLabel(); 314 | } 315 | }); 316 | 317 | // View(s) may be already loaded while the directive's linking 318 | renderLabel(); 319 | } 320 | }; 321 | 322 | } 323 | }; 324 | } 325 | BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 326 | 327 | function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { 328 | 329 | return { 330 | restrict: 'A', 331 | scope: {}, 332 | template: '{{ncyBreadcrumbChain}}', 333 | 334 | compile: function(cElement, cAttrs) { 335 | // Override the default template if ncyBreadcrumbText has a value 336 | var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); 337 | if(template) { 338 | cElement.html(template); 339 | } 340 | 341 | var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator) || ' / '; 342 | 343 | return { 344 | post: function postLink(scope) { 345 | var labelWatchers = []; 346 | 347 | var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope) { 348 | angular.forEach(getExpression(interpolationFunction), function(expression) { 349 | var watcher = viewScope.$watch(expression, function(newValue, oldValue) { 350 | if (newValue !== oldValue) { 351 | renderLabel(); 352 | } 353 | }); 354 | labelWatcherArray.push(watcher); 355 | }); 356 | }; 357 | 358 | var renderLabel = function() { 359 | deregisterWatchers(labelWatchers); 360 | labelWatchers = []; 361 | 362 | var viewScope = $breadcrumb.$getLastViewScope(); 363 | var steps = $breadcrumb.getStatesChain(); 364 | var combinedLabels = []; 365 | angular.forEach(steps, function (step) { 366 | if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { 367 | var parseLabel = $interpolate(step.ncyBreadcrumb.label); 368 | combinedLabels.push(parseLabel(viewScope)); 369 | // Watcher for further viewScope updates 370 | registerWatchersText(labelWatchers, parseLabel, viewScope); 371 | } else { 372 | combinedLabels.push(step.name); 373 | } 374 | }); 375 | 376 | scope.ncyBreadcrumbChain = combinedLabels.join(separator); 377 | }; 378 | 379 | registerListenerOnce('BreadcrumbTextDirective.$viewContentLoaded', $rootScope, '$viewContentLoaded', function (event) { 380 | if(!event.targetScope.ncyBreadcrumbIgnore) { 381 | renderLabel(); 382 | } 383 | }); 384 | 385 | // View(s) may be already loaded while the directive's linking 386 | renderLabel(); 387 | } 388 | }; 389 | 390 | } 391 | }; 392 | } 393 | BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; 394 | 395 | angular.module('ncy-angular-breadcrumb', ['ui.router.state']) 396 | .provider('$breadcrumb', $Breadcrumb) 397 | .directive('ncyBreadcrumb', BreadcrumbDirective) 398 | .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) 399 | .directive('ncyBreadcrumbText', BreadcrumbTextDirective); 400 | -------------------------------------------------------------------------------- /test/mock/test-logging.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | var disableStateChangeStartLogging = true; 4 | var disableStateChangeSuccessLogging = true; 5 | var disableStateChangeErrorLogging = false; 6 | 7 | beforeEach(module(function() { 8 | return function($rootScope) { 9 | 10 | $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error){ 11 | if(!disableStateChangeErrorLogging) { 12 | console.error('$stateChangeError', fromState.name, toState.name, error); 13 | } 14 | }); 15 | 16 | $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState){ 17 | if(!disableStateChangeSuccessLogging) { 18 | console.info('$stateChangeSuccess', fromState.name, toState.name); 19 | } 20 | }); 21 | 22 | $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState){ 23 | if(!disableStateChangeStartLogging) { 24 | console.info('$stateChangeStart', fromState.name, toState.name); 25 | } 26 | }); 27 | 28 | }; 29 | })); 30 | -------------------------------------------------------------------------------- /test/mock/test-modules.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | /** 4 | * Module with minimalist configuration. 5 | */ 6 | angular.module('ncy-basic-conf', []).config(function($stateProvider) { 7 | $stateProvider 8 | .state('A', {url: '/a', ncyBreadcrumb: {label: 'State A'}}) 9 | .state('A.B', {url: '/b', ncyBreadcrumb: {label: 'State B'}}) 10 | .state('A.B.C', {url: '/c', ncyBreadcrumb: {label: 'State C'}}) 11 | .state('D', {parent: 'A.B.C', url: '/d', ncyBreadcrumb: {label: 'State D'}}) // Explicit parent 12 | .state('D.E', {url: '/e', ncyBreadcrumb: {label: 'State E', skip: true}}) 13 | .state('D.E.F', {url: '/f', ncyBreadcrumb: {label: 'State F'}}) 14 | .state('G', {url: '/g', ncyBreadcrumb: {label: 'State G', skip: true}}) 15 | .state('G.H', {url: '/h', ncyBreadcrumb: {label: 'State H'}}); 16 | }); 17 | 18 | /** 19 | * Module including abstract states. 20 | */ 21 | angular.module('ncy-abstract-conf', []).config(function($stateProvider) { 22 | $stateProvider 23 | .state('A', {url: '/a', abstract: true, ncyBreadcrumb: {label: 'State A'}}) 24 | .state('A.B', {url: '/b', ncyBreadcrumb: {label: 'State B'}}) 25 | .state('A.B.C', {url: '/c', ncyBreadcrumb: {label: 'State C'}}) 26 | .state('D', {url: '/d', ncyBreadcrumb: {label: 'State D'}}) 27 | .state('D.E', {url: '/e', abstract: true, ncyBreadcrumb: {label: 'State E'}}) 28 | .state('D.E.F', {url: '/f', ncyBreadcrumb: {label: 'State F'}}) 29 | .state('G', {url: '/g', abstract: true, ncyBreadcrumb: {label: 'State G', skip: true}}) 30 | .state('G.H', {url: '/h', ncyBreadcrumb: {label: 'State H'}}) 31 | .state('I', {url: '/i', abstract: true, ncyBreadcrumb: {label: 'State I', force: true}}) 32 | .state('I.J', {url: '/j', ncyBreadcrumb: {label: 'State J'}}) 33 | .state('K', {url: '/k', abstract: true, ncyBreadcrumb: {label: 'State K', skip: true, force: true}}) 34 | .state('K.L', {url: '/l', ncyBreadcrumb: {label: 'State L'}}); 35 | }); 36 | 37 | /** 38 | * Module including parents defined by objects. 39 | */ 40 | angular.module('ncy-object-parent-conf', []).config(function($stateProvider) { 41 | var A = { 42 | name: 'A', 43 | url: '/a', 44 | ncyBreadcrumb: {label: 'State A'} 45 | }; 46 | var A_B = { 47 | name: 'B', 48 | url: '/b', 49 | ncyBreadcrumb: {label: 'State B'}, 50 | parent: A 51 | }; 52 | $stateProvider 53 | .state(A) 54 | .state(A_B); 55 | }); 56 | 57 | /** 58 | * Module with dynamic parent configuration. 59 | */ 60 | angular.module('ncy-dynamic-parent-conf', []).config(function($stateProvider) { 61 | $stateProvider 62 | .state('A', {url: '/a', ncyBreadcrumb: {label: 'State A'}}) 63 | .state('A.B', {url: '/b', ncyBreadcrumb: {label: 'State B'}}) 64 | .state('C', {url: '/c', ncyBreadcrumb: {label: 'State C'}}) 65 | .state('D', {url: '/d', ncyBreadcrumb: {label: 'State D'}}) 66 | .state('D.E', {url: '/e', ncyBreadcrumb: {label: 'State E'}}) 67 | .state('D.E.F', {url:'/f', ncyBreadcrumb: {label: 'State F', parent: 'A.B'}}) // Specific parent for breadcrumb 68 | .state('D.E.G', {url:'/g', ncyBreadcrumb: {label: 'State G', parent: function() { 69 | return 'A'; 70 | }}}) 71 | .state('D.E.H', {url:'/h', ncyBreadcrumb: {label: 'State H', parent: function($scope) { 72 | return $scope.parentState; 73 | }}}) 74 | .state('I', {url: '/i/:x/:y', ncyBreadcrumb: {label:'State I'}}) 75 | .state('J', {url: '/j', ncyBreadcrumb: {label:'State J', parent: 'I({x: \'love\', y: \'you\'})'}}); 76 | }).controller('UndefinedCtrl', function($scope) { 77 | $scope.parentState = undefined; 78 | }).controller('ReturnCCtrl', function($scope) { 79 | $scope.parentState = 'C'; 80 | }); 81 | 82 | /** 83 | * Module with custom template using ui-sref directive. 84 | */ 85 | angular.module('ncy-ui-sref-template-conf', []).config(function($stateProvider, $breadcrumbProvider) { 86 | $stateProvider 87 | .state('A', {url: '/a', ncyBreadcrumb: {label: 'State A'}}) 88 | .state('A.B', {url: '/b', ncyBreadcrumb: {label: 'State B'}}) 89 | .state('I', {url: '/i/:x/:y', ncyBreadcrumb: {label:'State I'}}) 90 | .state('J', {url: '/j', ncyBreadcrumb: {label:'State J', parent: 'I({x: \'love\', y: \'you\'})'}}) 91 | .state('K', {url: '/k', ncyBreadcrumb: {label:'State K', parent: function() { 92 | return 'I({x: \'love\', y: \'you\'})'; 93 | }}}); 94 | 95 | $breadcrumbProvider.setOptions({ 96 | template: '' 97 | }); 98 | }); 99 | 100 | /** 101 | * Module with angular expressions in label 102 | */ 103 | angular.module('ncy-interpolation-conf', []).config(function($stateProvider) { 104 | $stateProvider 105 | .state('A', {url: '/a', controller: 'ACtrl', template: '
    View A
    ', ncyBreadcrumb: {label: 'State A'}}) 106 | .state('A.B', {url: '/b', controller: 'BCtrl', template: '
    View B
    ', ncyBreadcrumb: {label: 'State {{tripleB}}'}}) 107 | .state('A.B.C', {url: '/c', ncyBreadcrumb: {label: 'State C'}}) // no controller 108 | .state('A.B.D', {url: '/d', controller: function($scope) {$scope.tripleD='DDD';}, template: '
    View D
    ', ncyBreadcrumb: {label: 'State {{tripleD}}'}}); // inline controller 109 | }).controller('ACtrl', function($scope) { 110 | $scope.tripleA = 'AAA'; 111 | }).controller('BCtrl', function($scope) { 112 | $scope.tripleB = 'BBB'; 113 | }); 114 | 115 | /** 116 | * Module with html configuration. 117 | */ 118 | angular.module('ncy-html-conf', ['ngSanitize']).config(function($stateProvider) { 119 | $stateProvider 120 | .state('html', {url: '/html', ncyBreadcrumb: {label: 'Html is interpreted'}}); 121 | }); 122 | 123 | angular.module('ncy-sample-conf', ['ncy-sample', 'ngMock']).config(function($urlRouterProvider) { 124 | // New module to override the otherwise of the sample app. 125 | // (we don't want to deal with the location according to each state) 126 | $urlRouterProvider.otherwise(function() { 127 | return false; 128 | }); 129 | }).run(function($httpBackend) { 130 | // Due to templateUrl definitions in $state configuration, 131 | // httpBackend will try to load the view for each state asked. 132 | $httpBackend.when('GET', 'views/home.html').respond('dummy home view'); 133 | $httpBackend.when('GET', 'views/sample.html').respond('dummy sample view'); 134 | $httpBackend.when('GET', 'views/room_list.html').respond('dummy room_list view'); 135 | $httpBackend.when('GET', 'views/room_detail.html').respond('dummy room_detail view'); 136 | $httpBackend.when('GET', 'views/room_form.html').respond('dummy room_form view'); 137 | }); 138 | 139 | /** 140 | * Module with multiple views related to one state and interpolated labels 141 | */ 142 | angular.module('ncy-multiple-interpolation-conf', []).config(function($stateProvider) { 143 | $stateProvider 144 | .state('A', { 145 | url: '/a', 146 | views: { 147 | 'a@': { 148 | template: '
    View A
    ', 149 | controller: 'ACtrl' 150 | } 151 | } 152 | }) 153 | .state('A.B', { 154 | url: '/b', 155 | views: { 156 | 'b@': { 157 | template: '
    View B
    ', 158 | controller: 'BCtrl' 159 | } 160 | }, 161 | ncyBreadcrumb: { 162 | label: 'State {{tripleB}}' 163 | } 164 | }); 165 | }).controller('ACtrl', function() { 166 | }).controller('BCtrl', function($scope) { 167 | $scope.tripleB = 'BBB'; 168 | }); 169 | -------------------------------------------------------------------------------- /test/mock/test-ui-router-sample.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | /** 4 | * Module with the ui-router sample conf. 5 | * Code is taken from https://github.com/angular-ui/ui-router/tree/master/sample 6 | * and has been merged in this functionnal module in order to test 7 | * the angular-breadcrumb with the sample of ui-router. 8 | */ 9 | angular.module('ncy-ui-router-conf', ['ngMock']) 10 | .factory('utils', function () { 11 | return { 12 | findById: function findById(a, id) { 13 | for (var i = 0; i < a.length; i++) { 14 | if (a[i].id === id) { 15 | return a[i]; 16 | } 17 | } 18 | return null; 19 | }, 20 | 21 | newRandomKey: function newRandomKey(coll, key, currentKey){ 22 | var randKey; 23 | do { 24 | randKey = coll[Math.floor(coll.length * Math.random())][key]; 25 | } while (randKey === currentKey); 26 | return randKey; 27 | } 28 | }; 29 | }) 30 | .factory('contacts', ['utils', function (utils) { 31 | var contacts = { 32 | "contacts":[ 33 | {"id": 1, "name": "Alice", "items": [ 34 | {"id": "a", "type": "phone number", "value": "555-1234-1234"}, 35 | {"id": "b", "type": "email", "value": "alice@mailinator.com"} 36 | ]}, 37 | {"id": 42, "name": "Bob", "items": [ 38 | {"id": "a", "type": "blog", "value": "http://bob.blogger.com"}, 39 | {"id": "b", "type": "fax", "value": "555-999-9999"} 40 | ]}, 41 | {"id": 123, "name": "Eve", "items": [ 42 | {"id": "a", "type": "full name", "value": "Eve Adamsdottir"} 43 | ]} 44 | ] 45 | }; 46 | var factory = {}; 47 | factory.all = function () { 48 | return contacts; 49 | }; 50 | factory.get = function (id) { 51 | return contacts.then(function(){ 52 | return utils.findById(contacts, id); 53 | }); 54 | }; 55 | return factory; 56 | }]) 57 | .config(function($stateProvider, $breadcrumbProvider) { 58 | $breadcrumbProvider.setOptions({ 59 | prefixStateName: 'home' 60 | }); 61 | 62 | $stateProvider.state("home", { 63 | url: "/", 64 | template: '

    Welcome to the UI-Router Demo

    ' + 65 | '

    Use the menu above to navigate. ' + 66 | 'Pay attention to the $state and $stateParams values below.

    ' + 67 | '

    Click these links—Alice or ' + 68 | 'Bob—to see a url redirect in action.

    ' 69 | 70 | }) 71 | .state('about', { 72 | url: '/about', 73 | templateProvider: ['$timeout', function ($timeout) { 74 | return $timeout(function () { 75 | return '

    UI-Router Resources

    '; 82 | }, 100); 83 | }] 84 | }) 85 | .state('contacts', { 86 | abstract: true, 87 | url: '/contacts', 88 | templateUrl: 'app/contacts/contacts.html', 89 | resolve: { 90 | contacts: ['contacts', 91 | function( contacts){ 92 | return contacts.all(); 93 | }] 94 | }, 95 | controller: ['$scope', '$state', 'contacts', 'utils', 96 | function ( $scope, $state, contacts, utils) { 97 | $scope.contacts = contacts; 98 | $scope.goToRandom = function () { 99 | var randId = utils.newRandomKey($scope.contacts, "id", $state.params.contactId); 100 | $state.go('contacts.detail', { contactId: randId }); 101 | }; 102 | }] 103 | }) 104 | .state('contacts.list', { 105 | url: '', 106 | templateUrl: 'app/contacts/contacts.list.html' 107 | }) 108 | .state('contacts.detail', { 109 | url: '/{contactId:[0-9]{1,4}}', 110 | views: { 111 | '': { 112 | templateUrl: 'app/contacts/contacts.detail.html', 113 | controller: ['$scope', '$stateParams', 'utils', 114 | function ( $scope, $stateParams, utils) { 115 | $scope.contact = utils.findById($scope.contacts, $stateParams.contactId); 116 | }] 117 | }, 118 | 'hint@': { 119 | template: 'This is contacts.detail populating the "hint" ui-view' 120 | }, 121 | 'menuTip': { 122 | templateProvider: ['$stateParams', 123 | function ($stateParams) { 124 | return '
    Contact ID: ' + $stateParams.contactId + ''; 125 | }] 126 | } 127 | }, 128 | ncyBreadcrumb: { 129 | parent: 'contacts.list' // Override the parent state (only for the breadcrumb). 130 | } 131 | }) 132 | .state('contacts.detail.item', { 133 | url: '/item/:itemId', 134 | views: { 135 | '': { 136 | templateUrl: 'app/contacts/contacts.detail.item.html', 137 | controller: ['$scope', '$stateParams', '$state', 'utils', 138 | function ( $scope, $stateParams, $state, utils) { 139 | $scope.item = utils.findById($scope.contact.items, $stateParams.itemId); 140 | 141 | $scope.edit = function () { 142 | $state.go('.edit', $stateParams); 143 | }; 144 | }] 145 | }, 146 | 'hint@': { 147 | template: ' This is contacts.detail.item overriding the "hint" ui-view' 148 | } 149 | } 150 | }) 151 | .state('contacts.detail.item.edit', { 152 | views: { 153 | '@contacts.detail': { 154 | templateUrl: 'app/contacts/contacts.detail.item.edit.html', 155 | controller: ['$scope', '$stateParams', '$state', 'utils', 156 | function ( $scope, $stateParams, $state, utils) { 157 | $scope.item = utils.findById($scope.contact.items, $stateParams.itemId); 158 | $scope.done = function () { 159 | $state.go('^', $stateParams); 160 | }; 161 | }] 162 | } 163 | } 164 | }); 165 | }) 166 | .run(function($httpBackend) { 167 | // Due to templateUrl definitions in $state configuration, 168 | // httpBackend will try to load the view for each state asked. 169 | $httpBackend.when('GET', 'app/contacts/contacts.html').respond('dummy contacts view'); 170 | $httpBackend.when('GET', 'app/contacts/contacts.list.html').respond('dummy contacts.list view'); 171 | $httpBackend.when('GET', 'app/contacts/contacts.detail.html').respond('dummy contacts.detail view'); 172 | $httpBackend.when('GET', 'app/contacts/contacts.detail.item.html').respond('dummy contacts.detail.item view'); 173 | $httpBackend.when('GET', 'app/contacts/contacts.detail.item.edit.html').respond('dummy contacts.detail.item.edit view'); 174 | }); 175 | -------------------------------------------------------------------------------- /test/mock/test-utils.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | var goToState, goToStateAndFlush, stringifyStateChain; 4 | 5 | beforeEach(function() { 6 | module('ncy-angular-breadcrumb', 'ui.router.state', 'ngMock', 'ng'); 7 | }); 8 | 9 | // Helpful function for tests : navigate to a state 10 | beforeEach(module(function() { 11 | return function($state, $rootScope) { 12 | goToState = function(state, stateParams) { 13 | $state.transitionTo(state, stateParams); 14 | $rootScope.$digest(); 15 | }; 16 | }; 17 | })); 18 | 19 | /** 20 | * Due to templateUrl definitions in $state configuration, 21 | * httpBackend will try to load the view for each state asked. 22 | */ 23 | beforeEach(module(function() { 24 | return function($httpBackend, $state) { 25 | 26 | // Helpful function for navigate within states. 27 | goToStateAndFlush = function(state, stateParams) { 28 | $state.transitionTo(state, stateParams); 29 | $httpBackend.flush(); 30 | }; 31 | }; 32 | })); 33 | 34 | stringifyStateChain = function(states) { 35 | var names = []; 36 | angular.forEach(states, function(state) { 37 | names.push(state.name); 38 | }); 39 | return names.join(' --> '); 40 | }; -------------------------------------------------------------------------------- /test/spec/conf-basic-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Simple conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-basic-conf'); 7 | }); 8 | 9 | it('must have some states defined', inject(function($state) { 10 | expect($state.get('A').name).toBe('A'); 11 | expect($state.get('A.B').name).toBe('A.B'); 12 | })); 13 | 14 | it('allows state transition in test', inject(function($state) { 15 | var oldState = $state.current.name; 16 | goToState('A.B'); 17 | var newState = $state.current.name; 18 | 19 | expect(newState).not.toBe(oldState); 20 | expect(newState).toBe('A.B'); 21 | })); 22 | 23 | }); -------------------------------------------------------------------------------- /test/spec/conf-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Sample conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-sample-conf'); 7 | }); 8 | 9 | it('must have some states defined', inject(function($state) { 10 | expect($state.get('home').name).toBe('home'); 11 | expect($state.get('room').name).toBe('room'); 12 | })); 13 | 14 | it('allows state transition in test', inject(function($state) { 15 | var oldState = $state.current.name; 16 | goToStateAndFlush('room'); 17 | var newState = $state.current.name; 18 | 19 | expect(newState).not.toBe(oldState); 20 | expect(newState).toBe('room'); 21 | })); 22 | 23 | }); -------------------------------------------------------------------------------- /test/spec/conf-ui-router-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Ui-router\'s sample conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-ui-router-conf'); 7 | }); 8 | 9 | it('must have some states defined', inject(function($state) { 10 | expect($state.get('home').name).toBe('home'); 11 | expect($state.get('contacts.list').name).toBe('contacts.list'); 12 | expect($state.get('contacts.detail.item.edit').name).toBe('contacts.detail.item.edit'); 13 | })); 14 | 15 | it('allows state transition in test', inject(function($state) { 16 | console.log('current : ', $state.current); 17 | 18 | var oldState = $state.current.name; 19 | goToStateAndFlush('contacts.list'); 20 | var newState = $state.current.name; 21 | 22 | expect(newState).not.toBe(oldState); 23 | expect(newState).toBe('contacts.list'); 24 | })); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/spec/directive-basic-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive with basic conf', function() { 4 | 5 | var element, scope; 6 | 7 | beforeEach(function() { 8 | module('ncy-basic-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile) { 12 | element = angular.element(''); 13 | var compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | compile(scope); 16 | scope.$digest(); 17 | })); 18 | 19 | it('renders the correct state chain', inject(function() { 20 | goToState('D'); 21 | scope.$emit('$viewContentLoaded'); 22 | scope.$digest(); 23 | 24 | console.info('Directive content : ' + element.text()); 25 | expect(element.text()).toContain('State A'); 26 | expect(element.text()).toContain('State B'); 27 | expect(element.text()).toContain('State C'); 28 | expect(element.text()).toContain('State D'); 29 | 30 | expect(element.children().length).toBe(4); 31 | expect(element.find('a').length).toBe(3); 32 | 33 | expect(element.find('a').eq(0).attr('href')).toBe('#/a'); 34 | expect(element.find('a').eq(1).attr('href')).toBe('#/a/b'); 35 | expect(element.find('a').eq(2).attr('href')).toBe('#/a/b/c'); 36 | })); 37 | 38 | it('should work with one state', inject(function() { 39 | goToState('A'); 40 | scope.$emit('$viewContentLoaded'); 41 | scope.$digest(); 42 | 43 | console.info('Directive content : ' + element.text()); 44 | expect(element.text()).toContain('State A'); 45 | 46 | expect(element.children().length).toBe(1); 47 | expect(element.find('a').length).toBe(0); 48 | })); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/directive-dynamic-parent-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive with dynamic parent conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-dynamic-parent-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element('
    '); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('should use the custom breadcrumb\'s parent property referencing a variable of the scope', inject(function() { 19 | goToState('D.E.H'); 20 | 21 | controller('ReturnCCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | expect(scope.parentState).toBeDefined(); 24 | 25 | scope.$emit('$viewContentLoaded'); 26 | scope.$digest(); 27 | 28 | console.info('Directive content : ' + element.text()); 29 | 30 | expect(element.text()).toContain('State C'); 31 | expect(element.text()).toContain('State H'); 32 | expect(element.text()).not.toContain('State E'); 33 | })); 34 | 35 | it('should ignore the custom breadcrumb\'s parent property if it is a function returning undefined', inject(function() { 36 | goToState('D.E.H'); 37 | 38 | controller('UndefinedCtrl', {'$scope' : scope} ); 39 | compile(scope); 40 | expect(scope.parentState).toBeUndefined(); 41 | 42 | scope.$emit('$viewContentLoaded'); 43 | scope.$digest(); 44 | 45 | console.info('Directive content : ' + element.text()); 46 | 47 | expect(element.text()).toContain('State D'); 48 | expect(element.text()).toContain('State E'); 49 | expect(element.text()).toContain('State H'); 50 | 51 | })); 52 | 53 | it('deals with url params correctly', inject(function() { 54 | goToState('J'); 55 | 56 | compile(scope); 57 | 58 | scope.$emit('$viewContentLoaded'); 59 | scope.$digest(); 60 | 61 | console.info('Directive content : ' + element.text()); 62 | 63 | expect(element.text()).toContain('State I'); 64 | expect(element.text()).toContain('State J'); 65 | 66 | expect(element.find('a').attr('href')).toBe('#/i/love/you'); 67 | })); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/spec/directive-interpolation-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive with interpolation conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-interpolation-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element('
    '); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('interpolates labels correctly', inject(function() { 19 | goToState('A.B'); 20 | 21 | controller('BCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | 24 | expect(scope.tripleB).toBeDefined(); 25 | 26 | scope.$emit('$viewContentLoaded'); 27 | scope.$digest(); 28 | 29 | console.info('Directive content : ' + element.text()); 30 | 31 | expect(element.text()).toContain('State BBB'); 32 | 33 | expect(element.find('a').eq(0).attr('href')).toBe('#/a'); 34 | })); 35 | 36 | it('deals with further updates of the scope', inject(function() { 37 | goToState('A.B'); 38 | 39 | controller('BCtrl', {'$scope' : scope} ); 40 | compile(scope); 41 | 42 | scope.$emit('$viewContentLoaded'); 43 | scope.$digest(); 44 | 45 | console.info('Directive content : ' + element.text()); 46 | 47 | expect(element.text()).toContain('State BBB'); 48 | 49 | scope.tripleB = 'HACKED'; 50 | scope.$digest(); 51 | 52 | expect(element.text()).toContain('State HACKED'); 53 | 54 | })); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/directive-last-basic-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Last step directive with basic conf', function() { 4 | 5 | var element, scope; 6 | 7 | beforeEach(function() { 8 | module('ncy-basic-conf'); 9 | }); 10 | 11 | describe('without template', function() { 12 | 13 | beforeEach(inject(function($rootScope, $compile) { 14 | element = angular.element(''); 15 | var compile = $compile(element); 16 | scope = $rootScope.$new(); 17 | compile(scope); 18 | scope.$digest(); 19 | })); 20 | 21 | it('renders the last step label correctly', inject(function() { 22 | goToState('D'); 23 | scope.$emit('$viewContentLoaded'); 24 | scope.$digest(); 25 | 26 | console.info('Directive content : ' + element.text()); 27 | expect(element.text()).toBe('State D'); 28 | })); 29 | 30 | }); 31 | 32 | describe('with template', function() { 33 | 34 | beforeEach(inject(function($rootScope, $compile) { 35 | element = angular.element(''); 36 | var compile = $compile(element); 37 | scope = $rootScope.$new(); 38 | compile(scope); 39 | scope.$digest(); 40 | })); 41 | 42 | it('renders the template correctly', inject(function() { 43 | goToState('D'); 44 | scope.$emit('$viewContentLoaded'); 45 | scope.$digest(); 46 | 47 | console.info('Directive content : ' + element.text()); 48 | expect(element.text()).toBe('State D|#/a/b/c/d'); 49 | })); 50 | 51 | }); 52 | 53 | describe('with custom template', function() { 54 | beforeEach(function() { 55 | angular.module('ncy-basic-conf') 56 | .config(function($breadcrumbProvider) { 57 | $breadcrumbProvider.setOptions({ 58 | templateLast: '{{ncyBreadcrumbLabel}}' 59 | }); 60 | }); 61 | module('ncy-basic-conf'); 62 | }); 63 | 64 | beforeEach(inject(function($rootScope, $compile) { 65 | element = angular.element(''); 66 | var compile = $compile(element); 67 | scope = $rootScope.$new(); 68 | compile(scope); 69 | scope.$digest(); 70 | })); 71 | 72 | it('correctly', inject(function() { 73 | goToState('D'); 74 | scope.$emit('$viewContentLoaded'); 75 | scope.$digest(); 76 | 77 | console.info('Directive content : ' + element.html()); 78 | expect(element[0].tagName.toLowerCase()).toBe('span'); 79 | expect(element.find('i').length).toBe(1); 80 | })); 81 | }); 82 | 83 | describe('uses custom template with html binding', function() { 84 | beforeEach(function() { 85 | angular.module('ncy-html-conf') 86 | .config(function($breadcrumbProvider) { 87 | $breadcrumbProvider.setOptions({ 88 | templateLast: '' 89 | }); 90 | }); 91 | module('ncy-html-conf'); 92 | }); 93 | 94 | beforeEach(inject(function($rootScope, $compile) { 95 | element = angular.element(''); 96 | var compile = $compile(element); 97 | scope = $rootScope.$new(); 98 | compile(scope); 99 | scope.$digest(); 100 | })); 101 | 102 | it('correctly', inject(function() { 103 | goToState('html'); 104 | scope.$emit('$viewContentLoaded'); 105 | scope.$digest(); 106 | 107 | console.info('Directive content : ' + element.html()); 108 | expect(element[0].tagName.toLowerCase()).toBe('span'); 109 | 110 | expect(element.find('i').length).toBe(2); 111 | expect(element.find('i').eq(0).text()).toBe('Html is interpreted'); 112 | expect(element.find('i').eq(1).text()).toBe('Html is interpreted'); 113 | })); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/spec/directive-last-interpolation-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Last step directive with interpolation conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-interpolation-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element(''); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('interpolates labels correctly', inject(function() { 19 | goToState('A.B'); 20 | 21 | controller('BCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | 24 | expect(scope.tripleB).toBeDefined(); 25 | 26 | scope.$emit('$viewContentLoaded'); 27 | scope.$digest(); 28 | 29 | console.info('Directive content : ' + element.text()); 30 | 31 | expect(element.text()).toBe('test|State BBB'); 32 | })); 33 | 34 | it('deals with further updates of the scope', inject(function() { 35 | goToState('A.B'); 36 | 37 | controller('BCtrl', {'$scope' : scope} ); 38 | compile(scope); 39 | 40 | scope.$emit('$viewContentLoaded'); 41 | scope.$digest(); 42 | 43 | console.info('Directive content : ' + element.text()); 44 | 45 | expect(element.text()).toBe('test|State BBB'); 46 | 47 | scope.tripleB = 'HACKED'; 48 | scope.$digest(); 49 | 50 | expect(element.text()).toBe('test|State HACKED'); 51 | 52 | })); 53 | 54 | 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/directive-last-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Last step directive with sample conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-sample-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element(''); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('interpolates "room.detail" label correctly', inject(function() { 19 | goToStateAndFlush('room.detail', {roomId: 3}); 20 | 21 | controller('RoomDetailCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | 24 | expect(scope.room).toBeDefined(); 25 | 26 | scope.$emit('$viewContentLoaded'); 27 | scope.$digest(); 28 | 29 | console.info('Directive content : ' + element.text()); 30 | 31 | expect(element.find('a').text()).toBe('Room 103'); 32 | expect(element.find('a').attr("href")).toBe('#/room/3'); 33 | })); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/directive-ncyBreadcrumbIgnore-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | 4 | var element, scope; 5 | 6 | describe('Breadcrumb directive with multiple-interpolation conf', function() { 7 | 8 | beforeEach(function () { 9 | module('ncy-multiple-interpolation-conf'); 10 | }); 11 | 12 | describe('when ncyBreadcrumbIgnore is undefined on parent view scope', function () { 13 | describe('when the parent view located before child view ', function () { 14 | beforeEach(inject(function ($rootScope, $compile) { 15 | element = angular.element('
    '); 16 | var compile = $compile(element); 17 | scope = $rootScope.$new(); 18 | compile(scope); 19 | scope.$digest(); 20 | })); 21 | 22 | it('renders the correct state chain and views content', inject(function () { 23 | goToState('A.B'); 24 | scope.$emit('$viewContentLoaded'); 25 | scope.$digest(); 26 | 27 | console.info('Directive content : ' + element.text()); 28 | 29 | expect(element.text()).toContain('AState BBBView AView B'); 30 | })); 31 | }); 32 | 33 | describe('when the parent view located after child view ', function () { 34 | beforeEach(inject(function ($rootScope, $compile) { 35 | element = angular.element('
    '); 36 | var compile = $compile(element); 37 | scope = $rootScope.$new(); 38 | compile(scope); 39 | scope.$digest(); 40 | })); 41 | 42 | it('renders the incorrect state chain', inject(function () { 43 | goToState('A.B'); 44 | scope.$emit('$viewContentLoaded'); 45 | scope.$digest(); 46 | 47 | console.info('Directive content : ' + element.text()); 48 | 49 | expect(element.text()).not.toContain('State BBB'); 50 | })); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('Breadcrumb directive with multiple-interpolation conf', function() { 56 | beforeEach(function () { 57 | module('ncy-multiple-interpolation-conf', function ($controllerProvider) { 58 | $controllerProvider.register('ACtrl', function ($scope) { 59 | $scope.ncyBreadcrumbIgnore = true; 60 | }); 61 | }); 62 | }); 63 | 64 | describe('when ncyBreadcrumbIgnore property equals true on parent view scope', function () { 65 | describe('when the parent view located before child view ', function () { 66 | beforeEach(inject(function ($rootScope, $compile) { 67 | element = angular.element('
    '); 68 | var compile = $compile(element); 69 | scope = $rootScope.$new(); 70 | 71 | compile(scope); 72 | scope.$digest(); 73 | })); 74 | 75 | it('renders the correct state chain and views content', inject(function () { 76 | goToState('A.B'); 77 | scope.$emit('$viewContentLoaded'); 78 | scope.$digest(); 79 | 80 | console.info('Directive content : ' + element.text()); 81 | 82 | expect(element.text()).toContain('AState BBBView AView B'); 83 | })); 84 | }); 85 | 86 | describe('when the parent view located after child view ', function () { 87 | beforeEach(inject(function ($rootScope, $compile) { 88 | element = angular.element('
    '); 89 | var compile = $compile(element); 90 | scope = $rootScope.$new(); 91 | 92 | compile(scope); 93 | scope.$digest(); 94 | })); 95 | 96 | it('renders the correct state chain and views content', inject(function () { 97 | goToState('A.B'); 98 | scope.$emit('$viewContentLoaded'); 99 | scope.$digest(); 100 | 101 | console.info('Directive content : ' + element.text()); 102 | 103 | expect(element.text()).toContain('AState BBBView BView A'); 104 | })); 105 | }); 106 | }); 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /test/spec/directive-object-parent-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive with object parent conf', function() { 4 | 5 | var element, scope; 6 | 7 | beforeEach(function() { 8 | module('ncy-object-parent-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile) { 12 | element = angular.element(''); 13 | var compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | compile(scope); 16 | scope.$digest(); 17 | })); 18 | 19 | it('should handle parents provided by object reference', inject(function() { 20 | goToState('B'); 21 | scope.$emit('$viewContentLoaded'); 22 | scope.$digest(); 23 | 24 | console.info('Directive content : ' + element.text()); 25 | expect(element.text()).toContain('State A'); 26 | expect(element.text()).toContain('State B'); 27 | })); 28 | 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/spec/directive-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive with sample conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-sample-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element('
    '); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('interpolates "room.detail" label correctly', inject(function() { 19 | goToStateAndFlush('room.detail.edit', {roomId: 3}); 20 | 21 | controller('RoomDetailCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | 24 | expect(scope.room).toBeDefined(); 25 | 26 | scope.$emit('$viewContentLoaded'); 27 | scope.$digest(); 28 | 29 | console.info('Directive content : ' + element.text()); 30 | 31 | expect(element.text()).toContain('Home'); 32 | expect(element.text()).toContain('Sample'); 33 | expect(element.text()).toContain('Rooms'); 34 | expect(element.text()).toContain('Room 103'); 35 | expect(element.text()).toContain('Editing'); 36 | 37 | expect(element.find('a').eq(0).attr('href')).toBe('#/home'); 38 | expect(element.find('a').eq(1).attr('href')).toBe('#/sample'); 39 | expect(element.find('a').eq(2).attr('href')).toBe('#/room'); 40 | expect(element.find('a').eq(3).attr('href')).toBe('#/room/3'); 41 | })); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/spec/directive-template-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive', function() { 4 | 5 | var element, scope; 6 | 7 | describe('uses default template (bootstrap3)', function() { 8 | beforeEach(function() { 9 | module('ncy-basic-conf'); 10 | }); 11 | 12 | beforeEach(inject(function($rootScope, $compile) { 13 | element = angular.element('
    '); 14 | var compile = $compile(element); 15 | scope = $rootScope.$new(); 16 | compile(scope); 17 | scope.$digest(); 18 | })); 19 | 20 | it('correctly', inject(function() { 21 | goToState('D'); 22 | scope.$emit('$viewContentLoaded'); 23 | scope.$digest(); 24 | 25 | console.info('Directive content : ' + element.text()); 26 | expect(element[0].tagName.toLowerCase()).toBe('ol'); 27 | 28 | expect(element.children().length).toBe(4); 29 | expect(element.find('a').length).toBe(3); 30 | })); 31 | 32 | }); 33 | 34 | describe('uses bootstrap2 template', function() { 35 | beforeEach(function() { 36 | angular.module('ncy-basic-conf') 37 | .config(function($breadcrumbProvider) { 38 | $breadcrumbProvider.setOptions({ 39 | template: 'bootstrap2' 40 | }); 41 | }); 42 | module('ncy-basic-conf'); 43 | }); 44 | 45 | beforeEach(inject(function($rootScope, $compile) { 46 | element = angular.element('
    '); 47 | var compile = $compile(element); 48 | scope = $rootScope.$new(); 49 | compile(scope); 50 | scope.$digest(); 51 | })); 52 | 53 | it('correctly', inject(function() { 54 | goToState('D'); 55 | scope.$emit('$viewContentLoaded'); 56 | scope.$digest(); 57 | 58 | console.info('Directive content : ' + element.text()); 59 | expect(element[0].tagName.toLowerCase()).toBe('ul'); 60 | 61 | expect(element.children().length).toBe(4); 62 | expect(element.find('a').length).toBe(3); 63 | })); 64 | }); 65 | 66 | describe('uses custom template', function() { 67 | beforeEach(function() { 68 | angular.module('ncy-basic-conf') 69 | .config(function($breadcrumbProvider) { 70 | $breadcrumbProvider.setOptions({ 71 | template: '' + 72 | '' + 73 | '' + 74 | '' + 75 | '' + 76 | '
    {{step.ncyBreadcrumbLabel}}{{step.ncyBreadcrumbLink}}
    ' 77 | }); 78 | }); 79 | module('ncy-basic-conf'); 80 | }); 81 | 82 | beforeEach(inject(function($rootScope, $compile) { 83 | element = angular.element('
    '); 84 | var compile = $compile(element); 85 | scope = $rootScope.$new(); 86 | compile(scope); 87 | scope.$digest(); 88 | })); 89 | 90 | it('correctly', inject(function() { 91 | goToState('D'); 92 | scope.$emit('$viewContentLoaded'); 93 | scope.$digest(); 94 | 95 | console.info('Directive content : ' + element.html()); 96 | expect(element[0].tagName.toLowerCase()).toBe('table'); 97 | 98 | expect(element.find('tr').length).toBe(4); 99 | expect(element.find('td').length).toBe(8); 100 | })); 101 | }); 102 | 103 | describe('uses custom template with html binding', function() { 104 | beforeEach(function() { 105 | angular.module('ncy-html-conf') 106 | .config(function($breadcrumbProvider) { 107 | $breadcrumbProvider.setOptions({ 108 | template: '
    ' + 109 | '' + 110 | '' + 111 | '
    ' 112 | }); 113 | }); 114 | module('ncy-html-conf'); 115 | }); 116 | 117 | beforeEach(inject(function($rootScope, $compile) { 118 | element = angular.element('
    '); 119 | var compile = $compile(element); 120 | scope = $rootScope.$new(); 121 | compile(scope); 122 | scope.$digest(); 123 | })); 124 | 125 | it('correctly', inject(function() { 126 | goToState('html'); 127 | scope.$emit('$viewContentLoaded'); 128 | scope.$digest(); 129 | 130 | console.info('Directive content : ' + element.html()); 131 | expect(element[0].tagName.toLowerCase()).toBe('div'); 132 | 133 | expect(element.find('span').length).toBe(2); 134 | expect(element.find('span').eq(0).text()).toBe('Html is interpreted'); 135 | expect(element.find('span').eq(1).text()).toBe('Html is interpreted'); 136 | })); 137 | }); 138 | 139 | }); 140 | -------------------------------------------------------------------------------- /test/spec/directive-text-basic-separator-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Text directive with separator with basic conf', function() { 4 | 5 | var element, scope; 6 | 7 | beforeEach(function() { 8 | module('ncy-basic-conf'); 9 | }); 10 | 11 | describe('without template', function() { 12 | 13 | beforeEach(inject(function($rootScope, $compile) { 14 | element = angular.element(''); 15 | var compile = $compile(element); 16 | scope = $rootScope.$new(); 17 | compile(scope); 18 | scope.$digest(); 19 | })); 20 | 21 | it('renders the text label correctly', inject(function() { 22 | goToState('D'); 23 | scope.$emit('$viewContentLoaded'); 24 | scope.$digest(); 25 | 26 | console.info('Directive content : ' + element.text()); 27 | expect(element.text()).toBe('State A>State B>State C>State D'); 28 | })); 29 | 30 | }); 31 | 32 | describe('with template', function() { 33 | 34 | beforeEach(inject(function($rootScope, $compile) { 35 | element = angular.element(''); 36 | var compile = $compile(element); 37 | scope = $rootScope.$new(); 38 | compile(scope); 39 | scope.$digest(); 40 | })); 41 | 42 | it('renders the template correctly', inject(function() { 43 | goToState('D'); 44 | scope.$emit('$viewContentLoaded'); 45 | scope.$digest(); 46 | 47 | console.info('Directive content : ' + element.text()); 48 | expect(element.text()).toBe('State A>State B>State C>State D - MyApp'); 49 | })); 50 | 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /test/spec/directive-text-basic-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Text directive with basic conf', function() { 4 | 5 | var element, scope; 6 | 7 | beforeEach(function() { 8 | module('ncy-basic-conf'); 9 | }); 10 | 11 | describe('without template', function() { 12 | 13 | beforeEach(inject(function($rootScope, $compile) { 14 | element = angular.element(''); 15 | var compile = $compile(element); 16 | scope = $rootScope.$new(); 17 | compile(scope); 18 | scope.$digest(); 19 | })); 20 | 21 | it('renders the text label correctly', inject(function() { 22 | goToState('D'); 23 | scope.$emit('$viewContentLoaded'); 24 | scope.$digest(); 25 | 26 | console.info('Directive content : ' + element.text()); 27 | expect(element.text()).toBe('State A / State B / State C / State D'); 28 | })); 29 | 30 | }); 31 | 32 | describe('with template', function() { 33 | 34 | beforeEach(inject(function($rootScope, $compile) { 35 | element = angular.element(''); 36 | var compile = $compile(element); 37 | scope = $rootScope.$new(); 38 | compile(scope); 39 | scope.$digest(); 40 | })); 41 | 42 | it('renders the template correctly', inject(function() { 43 | goToState('D'); 44 | scope.$emit('$viewContentLoaded'); 45 | scope.$digest(); 46 | 47 | console.info('Directive content : ' + element.text()); 48 | expect(element.text()).toBe('State A / State B / State C / State D - MyApp'); 49 | })); 50 | 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /test/spec/directive-text-interpolation-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Text directive with interpolation conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-interpolation-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element(''); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('interpolates labels correctly', inject(function() { 19 | goToState('A.B'); 20 | 21 | controller('BCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | 24 | expect(scope.tripleB).toBeDefined(); 25 | 26 | scope.$emit('$viewContentLoaded'); 27 | scope.$digest(); 28 | 29 | console.info('Directive content : ' + element.text()); 30 | 31 | expect(element.text()).toBe('test|State A / State BBB'); 32 | })); 33 | 34 | it('deals with further updates of the scope', inject(function() { 35 | goToState('A.B'); 36 | 37 | controller('BCtrl', {'$scope' : scope} ); 38 | compile(scope); 39 | 40 | scope.$emit('$viewContentLoaded'); 41 | scope.$digest(); 42 | 43 | console.info('Directive content : ' + element.text()); 44 | 45 | expect(element.text()).toBe('test|State A / State BBB'); 46 | 47 | scope.tripleB = 'HACKED'; 48 | scope.$digest(); 49 | 50 | expect(element.text()).toBe('test|State A / State HACKED'); 51 | 52 | })); 53 | 54 | 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/directive-text-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Text directive with sample conf', function() { 4 | 5 | var element, scope, controller, compile; 6 | 7 | beforeEach(function() { 8 | module('ncy-sample-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile, $controller) { 12 | element = angular.element(''); 13 | compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | controller = $controller; 16 | })); 17 | 18 | it('interpolates "room.detail" label correctly', inject(function() { 19 | goToStateAndFlush('room.detail', {roomId: 3}); 20 | 21 | controller('RoomDetailCtrl', {'$scope' : scope} ); 22 | compile(scope); 23 | 24 | expect(scope.room).toBeDefined(); 25 | 26 | scope.$emit('$viewContentLoaded'); 27 | scope.$digest(); 28 | 29 | console.info('Directive content : ' + element.text()); 30 | 31 | expect(element.text()).toBe('MyApp: Home / Sample / Rooms / Room 103'); 32 | })); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/spec/directive-ui-sref-template-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Breadcrumb directive with ui-sref template', function() { 4 | 5 | var element, scope; 6 | 7 | beforeEach(function() { 8 | module('ncy-ui-sref-template-conf'); 9 | }); 10 | 11 | beforeEach(inject(function($rootScope, $compile) { 12 | element = angular.element('
    '); 13 | var compile = $compile(element); 14 | scope = $rootScope.$new(); 15 | compile(scope); 16 | scope.$digest(); 17 | })); 18 | 19 | it('should work correctly', inject(function() { 20 | goToState('A.B'); 21 | scope.$emit('$viewContentLoaded'); 22 | scope.$digest(); 23 | 24 | console.info('Directive content : ' + element.text()); 25 | expect(element.text()).toContain('State A'); 26 | expect(element.text()).toContain('State B'); 27 | expect(element.find('a').eq(0).attr('href')).toBe('#/a'); 28 | expect(element.find('a').eq(1).attr('href')).toBe('#/a/b'); 29 | })); 30 | 31 | it('should deal with url params correctly', inject(function() { 32 | goToState('J'); 33 | scope.$emit('$viewContentLoaded'); 34 | scope.$digest(); 35 | 36 | console.info('Directive content : ' + element.text()); 37 | 38 | expect(element.text()).toContain('State I'); 39 | expect(element.text()).toContain('State J'); 40 | 41 | expect(element.find('a').eq(0).attr('href')).toBe('#/i/love/you'); 42 | })); 43 | 44 | 45 | it('should deal with url params correctly even with dynamic parent', inject(function() { 46 | goToState('K'); 47 | scope.$emit('$viewContentLoaded'); 48 | scope.$digest(); 49 | 50 | console.info('Directive content : ' + element.text()); 51 | 52 | expect(element.text()).toContain('State I'); 53 | expect(element.text()).toContain('State K'); 54 | 55 | expect(element.find('a').eq(0).attr('href')).toBe('#/i/love/you'); 56 | })); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /test/spec/scope-compare-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('The scope', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-basic-conf'); 7 | }); 8 | 9 | it('00A is older than 001', function() { 10 | expect(isAOlderThanB('00A', '001')).toBe(true); 11 | }); 12 | 13 | it('010 is older than 00Y', function() { 14 | expect(isAOlderThanB('010', '00Y')).toBe(true); 15 | }); 16 | 17 | it('01P is older than 010', function() { 18 | expect(isAOlderThanB('01P', '010')).toBe(true); 19 | }); 20 | 21 | it('FOO is older than BAR', function() { 22 | expect(isAOlderThanB('FOO', 'BAR')).toBe(true); 23 | }); 24 | 25 | it('F00 is older than BAR', function() { 26 | expect(isAOlderThanB('F00', 'BAR')).toBe(true); 27 | }); 28 | 29 | it('0000 is older than ZZZ', function() { 30 | expect(isAOlderThanB('0000', 'ZZZ')).toBe(true); 31 | }); 32 | 33 | it('(newly created) is always older than the precedent one', inject(function($rootScope) { 34 | var scope = $rootScope.$new(); 35 | for(var i = 0; i < 100000; i++) { 36 | var newScope = $rootScope.$new(); 37 | var isOlder = isAOlderThanB(newScope.$id, scope.$id); 38 | expect(isOlder).toBe(true); 39 | if(!isOlder) { 40 | console.log(newScope.$id, scope.$id, isOlder, i); 41 | break; 42 | } 43 | scope = newScope; 44 | } 45 | })); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /test/spec/service-abstract-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Service with abstract conf', function() { 4 | 5 | describe('with default options', function() { 6 | 7 | beforeEach(function() { 8 | module('ncy-abstract-conf'); 9 | }); 10 | 11 | it('should have a 2-step route to C state', inject(function($breadcrumb) { 12 | goToState('A.B.C'); 13 | var statesChain = $breadcrumb.getStatesChain(); 14 | expect(stringifyStateChain(statesChain)).toBe('A.B --> A.B.C'); 15 | })); 16 | 17 | it('should have a 2-step route to F state', inject(function($breadcrumb) { 18 | goToState('D.E.F'); 19 | var statesChain = $breadcrumb.getStatesChain(); 20 | expect(stringifyStateChain(statesChain)).toBe('D --> D.E.F'); 21 | })); 22 | 23 | it('should return a one step chain to G.H', inject(function($breadcrumb) { 24 | goToState('G.H'); 25 | var statesChain = $breadcrumb.getStatesChain(); 26 | expect(stringifyStateChain(statesChain)).toBe('G.H'); 27 | })); 28 | 29 | it('should return a two step route to I.J', inject(function($breadcrumb) { 30 | goToState('I.J'); 31 | var statesChain = $breadcrumb.getStatesChain(); 32 | expect(stringifyStateChain(statesChain)).toBe('I --> I.J'); 33 | })); 34 | 35 | it('should return a one step chain to K.L', inject(function($breadcrumb) { 36 | goToState('K.L'); 37 | var statesChain = $breadcrumb.getStatesChain(); 38 | expect(stringifyStateChain(statesChain)).toBe('K.L'); 39 | })); 40 | }); 41 | 42 | describe('with abstract state inclusion', function() { 43 | 44 | beforeEach(function() { 45 | angular.module('ncy-abstract-conf') 46 | .config(function($breadcrumbProvider) { 47 | $breadcrumbProvider.setOptions({ 48 | includeAbstract: true 49 | }); 50 | }); 51 | module('ncy-abstract-conf'); 52 | }); 53 | 54 | it('should have a 3-step route to C state', inject(function($breadcrumb) { 55 | goToState('A.B.C'); 56 | var statesChain = $breadcrumb.getStatesChain(); 57 | expect(stringifyStateChain(statesChain)).toBe('A --> A.B --> A.B.C'); 58 | })); 59 | 60 | it('should have a 3-step route to F state', inject(function($breadcrumb) { 61 | goToState('D.E.F'); 62 | var statesChain = $breadcrumb.getStatesChain(); 63 | expect(stringifyStateChain(statesChain)).toBe('D --> D.E --> D.E.F'); 64 | })); 65 | 66 | it('should still return a one step chain to G.H (state-level option skip is priority)', inject(function($breadcrumb) { 67 | goToState('G.H'); 68 | var statesChain = $breadcrumb.getStatesChain(); 69 | expect(stringifyStateChain(statesChain)).toBe('G.H'); 70 | })); 71 | }); 72 | 73 | 74 | 75 | 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /test/spec/service-basic-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Service with basic conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-basic-conf'); 7 | }); 8 | 9 | it('must be defined', inject(function($breadcrumb) { 10 | expect($breadcrumb).toBeDefined(); 11 | })); 12 | 13 | it('should have a 3-step route to C state', inject(function($breadcrumb) { 14 | goToState('A.B.C'); 15 | var statesChain = $breadcrumb.getStatesChain(); 16 | expect(stringifyStateChain(statesChain)).toBe('A --> A.B --> A.B.C'); 17 | 18 | var lastStep = $breadcrumb.getLastStep(); 19 | expect(lastStep.name).toBe('A.B.C'); 20 | })); 21 | 22 | it('should work also with "parent" state\'s property', inject(function($breadcrumb) { 23 | goToState('D'); 24 | var statesChain = $breadcrumb.getStatesChain(); 25 | expect(stringifyStateChain(statesChain)).toBe('A --> A.B --> A.B.C --> D'); 26 | 27 | var lastStep = $breadcrumb.getLastStep(); 28 | expect(lastStep.name).toBe('D'); 29 | })); 30 | 31 | it('must build a correct link for each steps', inject(function($breadcrumb) { 32 | goToState('D'); 33 | var statesChain = $breadcrumb.getStatesChain(); 34 | expect(statesChain[0].ncyBreadcrumbLink).toBe('#/a'); 35 | expect(statesChain[1].ncyBreadcrumbLink).toBe('#/a/b'); 36 | expect(statesChain[2].ncyBreadcrumbLink).toBe('#/a/b/c'); 37 | expect(statesChain[3].ncyBreadcrumbLink).toBe('#/a/b/c/d'); 38 | })); 39 | 40 | it('should expose the state conf', inject(function($breadcrumb) { 41 | goToState('A.B'); 42 | var lastStep = $breadcrumb.getLastStep(); 43 | expect(lastStep.ncyBreadcrumbStateRef).toBe('A.B'); 44 | })); 45 | 46 | it('should not return the step for E state', inject(function($breadcrumb) { 47 | goToState('D.E'); 48 | var statesChain = $breadcrumb.getStatesChain(); 49 | expect(stringifyStateChain(statesChain)).toBe('A --> A.B --> A.B.C --> D'); 50 | 51 | var lastStep = $breadcrumb.getLastStep(); 52 | expect(lastStep.name).toBe('D'); 53 | })); 54 | 55 | it('should have a 5-step route to F state (E skipped)', inject(function($breadcrumb) { 56 | goToState('D.E.F'); 57 | var statesChain = $breadcrumb.getStatesChain(); 58 | expect(stringifyStateChain(statesChain)).toBe('A --> A.B --> A.B.C --> D --> D.E.F'); 59 | })); 60 | 61 | it('should return an empty array for skipped G', inject(function($breadcrumb) { 62 | goToState('G'); 63 | var statesChain = $breadcrumb.getStatesChain(); 64 | expect(stringifyStateChain(statesChain)).toBe(''); 65 | 66 | var lastStep = $breadcrumb.getLastStep(); 67 | expect(lastStep).toBeUndefined(); 68 | })); 69 | 70 | it('should return a one step chain to G.H', inject(function($breadcrumb) { 71 | goToState('G.H'); 72 | var statesChain = $breadcrumb.getStatesChain(); 73 | expect(stringifyStateChain(statesChain)).toBe('G.H'); 74 | 75 | var lastStep = $breadcrumb.getLastStep(); 76 | expect(lastStep.name).toBe('G.H'); 77 | })); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /test/spec/service-dynamic-parent-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Service with dynamic parent conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-dynamic-parent-conf'); 7 | }); 8 | 9 | it('must be defined', inject(function($breadcrumb) { 10 | expect($breadcrumb).toBeDefined(); 11 | })); 12 | 13 | it('should use the custom breadcrumb\'s parent property for D.E.F (a string)', inject(function($breadcrumb) { 14 | goToState('D.E.F'); 15 | var statesChain = $breadcrumb.getStatesChain(); 16 | expect(stringifyStateChain(statesChain)).toBe('A --> A.B --> D.E.F'); 17 | })); 18 | 19 | it('should use the custom breadcrumb\'s parent property for D.E.G (a function)', inject(function($breadcrumb) { 20 | goToState('D.E.G'); 21 | var statesChain = $breadcrumb.getStatesChain(); 22 | expect(stringifyStateChain(statesChain)).toBe('A --> D.E.G'); 23 | })); 24 | 25 | it('should deals with url params correctly', inject(function($breadcrumb) { 26 | goToState('J'); 27 | var statesChain = $breadcrumb.getStatesChain(); 28 | expect(stringifyStateChain(statesChain)).toBe('I --> J'); 29 | expect(statesChain[0].name).toBe('I'); 30 | expect(statesChain[0].ncyBreadcrumbLink).toBe('#/i/love/you'); 31 | expect(statesChain[0].ncyBreadcrumbStateRef).toBe('I({x: \'love\', y: \'you\'})'); 32 | })); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/spec/service-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Service with sample conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-sample-conf'); 7 | }); 8 | 9 | it('generate a unique step for the "home" state', inject(function($breadcrumb) { 10 | goToStateAndFlush('home'); 11 | var statesChain = $breadcrumb.getStatesChain(); 12 | 13 | expect(stringifyStateChain(statesChain)).toBe('home'); 14 | 15 | var lastStep = $breadcrumb.getLastStep(); 16 | expect(lastStep.name).toBe('home'); 17 | })); 18 | 19 | it('generate three steps for the "room" state', inject(function($breadcrumb) { 20 | goToStateAndFlush('room'); 21 | var statesChain = $breadcrumb.getStatesChain(); 22 | 23 | expect(stringifyStateChain(statesChain)).toBe('home --> sample --> room'); 24 | 25 | var lastStep = $breadcrumb.getLastStep(); 26 | expect(lastStep.name).toBe('room'); 27 | })); 28 | 29 | it('generate four steps for the "room.detail" state', inject(function($breadcrumb) { 30 | goToStateAndFlush('room.detail', {roomId: 1}); 31 | var statesChain = $breadcrumb.getStatesChain(); 32 | 33 | expect(stringifyStateChain(statesChain)).toBe('home --> sample --> room --> room.detail'); 34 | 35 | var lastStep = $breadcrumb.getLastStep(); 36 | expect(lastStep.name).toBe('room.detail'); 37 | })); 38 | 39 | it('generate four steps for the "room.detail.edit" state with working links', inject(function($breadcrumb) { 40 | goToStateAndFlush('room.detail.edit', {roomId: 1}); 41 | var statesChain = $breadcrumb.getStatesChain(); 42 | 43 | expect(stringifyStateChain(statesChain)).toBe('home --> sample --> room --> room.detail --> room.detail.edit'); 44 | expect(statesChain[3].ncyBreadcrumbLink).toBe('#/room/1'); 45 | expect(statesChain[4].ncyBreadcrumbLink).toBe('#/room/1/edit'); 46 | })); 47 | 48 | it('must build a correct link for each steps', inject(function($breadcrumb) { 49 | goToStateAndFlush('room'); 50 | var statesChain = $breadcrumb.getStatesChain(); 51 | expect(statesChain[0].ncyBreadcrumbLink).toBe('#/home'); 52 | expect(statesChain[1].ncyBreadcrumbLink).toBe('#/sample'); 53 | })); 54 | }); 55 | -------------------------------------------------------------------------------- /test/spec/service-ui-router-sample-test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef: false */ 2 | 3 | describe('Service with ui-router\'s sample conf', function() { 4 | 5 | beforeEach(function() { 6 | module('ncy-ui-router-conf'); 7 | }); 8 | 9 | it('generate a unique step for the "home" state', inject(function($breadcrumb) { 10 | goToState('home'); 11 | var statesChain = $breadcrumb.getStatesChain(); 12 | 13 | expect(stringifyStateChain(statesChain)).toBe('home'); 14 | 15 | var lastStep = $breadcrumb.getLastStep(); 16 | expect(lastStep.name).toBe('home'); 17 | })); 18 | 19 | it('generate two steps for the "contacts.list" state', inject(function($breadcrumb) { 20 | goToStateAndFlush('contacts.list'); 21 | var statesChain = $breadcrumb.getStatesChain(); 22 | 23 | expect(stringifyStateChain(statesChain)).toBe('home --> contacts.list'); 24 | 25 | var lastStep = $breadcrumb.getLastStep(); 26 | expect(lastStep.name).toBe('contacts.list'); 27 | })); 28 | 29 | it('generate three steps for the "contacts.detail" state', inject(function($breadcrumb) { 30 | goToStateAndFlush('contacts.detail', {contactId: 42}); 31 | var statesChain = $breadcrumb.getStatesChain(); 32 | 33 | expect(stringifyStateChain(statesChain)).toBe('home --> contacts.list --> contacts.detail'); 34 | 35 | var lastStep = $breadcrumb.getLastStep(); 36 | expect(lastStep.name).toBe('contacts.detail'); 37 | })); 38 | 39 | it('generate four steps for the "contacts.detail.item" state', inject(function($breadcrumb) { 40 | goToStateAndFlush('contacts.detail.item', {contactId: 42, itemId: "a"}); 41 | var statesChain = $breadcrumb.getStatesChain(); 42 | 43 | expect(stringifyStateChain(statesChain)).toBe('home --> contacts.list --> contacts.detail --> contacts.detail.item'); 44 | 45 | 46 | var lastStep = $breadcrumb.getLastStep(); 47 | expect(lastStep.name).toBe('contacts.detail.item'); 48 | })); 49 | 50 | it('generate five steps for the "contacts.detail.item.edit" state with working links', inject(function($breadcrumb) { 51 | goToStateAndFlush('contacts.detail.item.edit', {contactId: 42, itemId: "a"}); 52 | var statesChain = $breadcrumb.getStatesChain(); 53 | 54 | expect(stringifyStateChain(statesChain)).toBe('home --> contacts.list --> contacts.detail --> contacts.detail.item --> contacts.detail.item.edit'); 55 | expect(statesChain[2].ncyBreadcrumbLink).toBe('#/contacts/42'); 56 | expect(statesChain[3].ncyBreadcrumbLink).toBe('#/contacts/42/item/a'); 57 | expect(statesChain[4].ncyBreadcrumbLink).toBe('#/contacts/42/item/a'); // (state with no URL) 58 | })); 59 | 60 | }); 61 | --------------------------------------------------------------------------------