diff --git a/awx/ui/client/lib/angular-breadcrumb/.bower.json b/awx/ui/client/lib/angular-breadcrumb/.bower.json new file mode 100644 index 0000000000..3895c9f0a9 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/.bower.json @@ -0,0 +1,44 @@ +{ + "name": "angular-breadcrumb", + "description": "AngularJS module that generates a breadcrumb from ui-router's states", + "version": "0.4.1", + "main": "release/angular-breadcrumb.js", + "ignore": [ + "sample", + "src", + "test", + ".bowerrc", + ".coveralls.yml", + ".gitignore", + ".jshintrc", + ".travis.yml", + "gruntfile.js", + "bower.json", + "karma.conf.js", + "libpeerconnection.log", + "package.json", + "README.md" + ], + "dependencies": { + "angular": ">=1.0.8", + "angular-ui-router": ">=0.2.0" + }, + "devDependencies": { + "bootstrap": "~2.3.2", + "angular-ui-bootstrap-bower": "~0.8.0", + "underscore": "~1.5.1", + "angular-mocks": ">=1.0.8", + "angular-sanitize": ">=1.0.8" + }, + "homepage": "https://github.com/ncuillery/angular-breadcrumb", + "_release": "0.4.1", + "_resolution": { + "type": "version", + "tag": "v0.4.1", + "commit": "b291e06f4010ebebbb41ea2c14e73e236aa70930" + }, + "_source": "git://github.com/ncuillery/angular-breadcrumb.git", + "_target": "~0.4.1", + "_originalSource": "angular-breadcrumb", + "_direct": true +} \ No newline at end of file diff --git a/awx/ui/client/lib/angular-breadcrumb/.editorconfig b/awx/ui/client/lib/angular-breadcrumb/.editorconfig new file mode 100644 index 0000000000..16480f1870 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/awx/ui/client/lib/angular-breadcrumb/.npmignore b/awx/ui/client/lib/angular-breadcrumb/.npmignore new file mode 100644 index 0000000000..86b98729db --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/.npmignore @@ -0,0 +1,15 @@ +sample +src +test +.idea +bower_components +coverage +testDependencies +.bowerrc +.coveralls.yml +.gitignore +.jshintrc +.travis.yml +gruntfile.js +karma.conf.js +libpeerconnection.log diff --git a/awx/ui/client/lib/angular-breadcrumb/CHANGELOG.md b/awx/ui/client/lib/angular-breadcrumb/CHANGELOG.md new file mode 100644 index 0000000000..6bec7b2db5 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/CHANGELOG.md @@ -0,0 +1,151 @@ + +### 0.4.1 (2015-08-09) + + +#### Features + +* 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)) + + + +### 0.4.0 (2015-05-17) + + +#### Bug Fixes + +* **$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)) +* **ncyBreadcrumb:** + * 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)) + * 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)) + +#### Features + +* Add force to ncyBreadcrumb options ([31125a38](http://github.com/ncuillery/angular-breadcrumb/commit/31125a386d706dd76df807b3b02e1fccea38fb59), closes [#77](http://github.com/ncuillery/angular-breadcrumb/issues/78)) +* **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)) + + + +### 0.3.3 (2014-12-16) + + +#### Bug Fixes + +* **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)) + + + +### 0.3.2 (2014-11-15) + +* **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)). + + +### 0.3.1 (2014-11-15) + + +#### Bug Fixes + +* **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)) +* **sample:** Send correct url params for the room link in booking view ([876de49a](http://github.com/ncuillery/angular-breadcrumb/commit/876de49a9c5d6e2d75714a606238e9041ed49baf)) + + + +## 0.3.0 (2014-10-29) + + +#### Bug Fixes + +* 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)) +* curly braces appearing on title of sample app ([855e76cb](http://github.com/ncuillery/angular-breadcrumb/commit/855e76cb33fda607fa3caa230564b77b48262c40)) + + +#### Features + +* 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)) +* **$breadcrumb:** + * 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)) + * 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)) +* **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)) + + +#### Breaking Changes + +* state-level options has been moved under the custom key +`ncyBreadcrumb` in state's configuration. + +To migrate the code follow the example below: +``` +// Before +$stateProvider.state('A', { + url: '/a', + data: { + ncyBreadcrumbLabel: 'State A' + } +}); +``` + +``` +// After +$stateProvider.state('A', { + url: '/a', + ncyBreadcrumb: { + label: 'State A' + } +}); +``` +See [API reference](https://github.com/ncuillery/angular-breadcrumb/wiki/API-Reference) for more informations. + ([1ea436d3](http://github.com/ncuillery/angular-breadcrumb/commit/1ea436d3f6d5470b7ae3e71e71259dbd2422bc00)) + + + +### 0.2.3 (2014-07-26) + + +#### Bug Fixes + +* **$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)) + + + +### 0.2.2 (2014-06-23) + + +#### Bug Fixes + +* catch the `$viewContentLoaded` earlier ([bb47dd54](http://github.com/ncuillery/angular-breadcrumb/commit/bb47dd54deb5efc579ccb9b1575e686803dee1c5), closes [#14](http://github.com/ncuillery/angular-breadcrumb/issues/14)) +* **sample:** + * make the CRU(D) about rooms working ([3ca89ec7](http://github.com/ncuillery/angular-breadcrumb/commit/3ca89ec771fd20dc4ab2d733612bdcfb96ced703)) + * 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)) + * 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)) + + +#### Features + +* **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)) + + + +### 0.2.1 (2014-05-16) + + +#### Bug Fixes + +* **$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)) + + + +### 0.2.0 (2014-05-08) + + +#### Bug Fixes + +* **$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)) +* **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)) + + +#### Features + +* **$breadcrumb:** + * 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)) + * allow chain of states customization ([028e493a](http://github.com/ncuillery/angular-breadcrumb/commit/028e493a1ebcae5ae60b8a9d42b949262000d7df), closes [#7](http://github.com/ncuillery/angular-breadcrumb/issues/7)) +* **ncyBreadcrumb:** add 'Element' declaration style '' ([b51441ea](http://github.com/ncuillery/angular-breadcrumb/commit/b51441eafb1659b782fea1f8668c7f455e1d6b4d)) + diff --git a/awx/ui/client/lib/angular-breadcrumb/CONTRIBUTING.md b/awx/ui/client/lib/angular-breadcrumb/CONTRIBUTING.md new file mode 100644 index 0000000000..720c4f142a --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing to angular-breadcrumb + +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. + +## Development hints + +### Installation +- Checkout the repository +- Run `npm install` +- Run `bower install` + +### Test running +This module uses the classic AngularJS stack with: + +- Karma (test runner) +- Jasmine (assertion framework) +- angular-mocks (AngularJS module for testing) + +Run the test with the grunt task `grunt test`. It runs the tests with different versions of AngularJS. + +### Test developing +Tests are build around modules with a specific `$stateProvider` configuration: + +- [Basic configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L6): Basic definitions (no template, no controller) +- [Interpolation configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L21): States with bindings in `ncyBreadcrumbLabel` +- [HTML configuration](https://github.com/ncuillery/angular-breadcrumb/blob/master/test/mock/test-modules.js#L36): States with HTML in `ncyBreadcrumbLabel` +- [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 +- [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) + +Theses modules are loaded by Karma and they are available in test specifications. + +Specifications are generally related to the directive `ncyBreadcrumb` or the service `$breadcrumb`. + +### Sample +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. + +## Submitting a Pull Request +- Fork the [repository](https://github.com/ncuillery/angular-breadcrumb/) +- Make your changes in a new git branch following the coding rules below. +- Run the grunt default task (by typing `grunt` or `grunt default`): it will run the tests and build the module in `dist` directory) +- Commit the changes (including the `dist` directory) by using the commit conventions explained below. +- Push and make the PR + + +## Coding rules +- When making changes on the source file, please check that your changes are covered by the tests. If not, create a new test case. + + +## Commit conventions +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). + +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)). diff --git a/awx/ui/client/lib/angular-breadcrumb/Gruntfile.js b/awx/ui/client/lib/angular-breadcrumb/Gruntfile.js new file mode 100644 index 0000000000..744af4d211 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/Gruntfile.js @@ -0,0 +1,258 @@ +'use strict'; + +var LIVERELOAD_PORT = 35729; +var lrSnippet = require('connect-livereload')({ port: LIVERELOAD_PORT }); +var mountFolder = function (connect, dir) { + return connect.static(require('path').resolve(dir)); +}; + +module.exports = function (grunt) { + + // Project configuration. + grunt.initConfig({ + // Metadata. + pkg: grunt.file.readJSON('package.json'), + headerDev: '/*! <%= pkg.name %> - v<%= pkg.version %>-dev-<%= grunt.template.today("yyyy-mm-dd") %>\n', + headerRelease: '/*! <%= pkg.name %> - v<%= pkg.version %>\n', + banner: '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + + '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + + ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n', + // Task configuration. + concat: { + dev: { + options: { + banner: '<%= headerDev %><%= banner %>\n(function (window, angular, undefined) {\n', + footer: '})(window, window.angular);\n', + stripBanners: true + }, + src: ['src/<%= pkg.name %>.js'], + dest: 'dist/<%= pkg.name %>.js' + }, + release: { + options: { + banner: '<%= headerRelease %><%= banner %>\n(function (window, angular, undefined) {\n', + footer: '})(window, window.angular);\n', + stripBanners: true + }, + src: ['src/<%= pkg.name %>.js'], + dest: 'release/<%= pkg.name %>.js' + } + }, + uglify: { + dev: { + options: { + banner: '<%= headerDev %><%= banner %>' + }, + src: '<%= concat.dev.dest %>', + dest: 'dist/<%= pkg.name %>.min.js' + }, + release: { + options: { + banner: '<%= headerRelease %><%= banner %>' + }, + src: '<%= concat.release.dest %>', + dest: 'release/<%= pkg.name %>.min.js' + } + }, + karma: { + unit: { + configFile: 'karma.conf.js' + } + }, + jshint: { + options: { + jshintrc: '.jshintrc' + }, + gruntfile: { + src: 'Gruntfile.js' + }, + sources: { + options: { + jshintrc: 'src/.jshintrc' + }, + src: ['src/**/*.js'] + }, + test: { + src: ['test/**/*.js'] + } + }, + watch: { + gruntfile: { + files: '<%= jshint.gruntfile.src %>', + tasks: ['jshint:gruntfile'] + }, + sources: { + files: '<%= jshint.sources.src %>', + tasks: ['jshint:sources', 'karma'] + }, + test: { + files: '<%= jshint.test.src %>', + tasks: ['jshint:test', 'karma'] + }, + sample: { + options: { + livereload: LIVERELOAD_PORT + }, + tasks: 'copy:breadcrumb', + files: [ + 'sample/*.{css,js,html}', + 'sample/controllers/*.{css,js,html}', + 'sample/views/*.{css,js,html}', + 'src/*.js' + ] + } + }, + copy: { + breadcrumb: { + files: [ + { + flatten: true, + expand: true, + src: [ + 'src/angular-breadcrumb.js' + ], + dest: 'sample/asset/' + } + ] + }, + asset: { + files: [ + { + flatten: true, + expand: true, + src: [ + 'dist/angular-breadcrumb.js', + 'bower_components/angular/angular.js', + 'bower_components/angular-ui-router/release/angular-ui-router.js', + 'bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js', + 'bower_components/bootstrap/docs/assets/css/bootstrap.css', + 'bower_components/underscore/underscore.js' + ], + dest: 'sample/asset/' + } + ] + }, + img: { + files: [ + { + flatten: true, + expand: true, + src: [ + 'bower_components/bootstrap.css/img/glyphicons-halflings.png' + ], + dest: 'sample/img/' + } + ] + } + }, + connect: { + options: { + port: 9000, + hostname: 'localhost' + }, + livereload: { + options: { + middleware: function (connect) { + return [ + lrSnippet, + mountFolder(connect, 'sample') + ]; + } + } + } + }, + open: { + server: { + url: 'http://localhost:<%= connect.options.port %>/index.html' + } + }, + bump: { + options: { + files: ['package.json', 'bower.json'], + updateConfigs: ['pkg'] + } + }, + clean: { + release: ["sample/*.zip"], + test: ["testDependencies/*"] + }, + compress: { + release: { + options: { + archive: 'sample/<%= pkg.name %>-<%= pkg.version %>.zip' + }, + files: [ + {expand: true, cwd: 'release/', src: ['*.js']} + ] + } + }, + replace: { + release: { + src: ['sample/views/home.html'], + overwrite: true, + replacements: [{ + from: /angular-breadcrumb-[0-9]+\.[0-9]+\.[0-9]+\.zip/g, + to: "angular-breadcrumb-<%= pkg.version %>.zip" + }, + { + from: /\([0-9]+\.[0-9]+\.[0-9]+\)/g, + to: "(<%= pkg.version %>)" + }] + } + }, + shell: { + testMinimal: { + command: 'bower install angular#=1.0.8 angular-mocks#=1.0.8 angular-sanitize#=1.0.8 angular-ui-router#=0.2.0 --config.directory=. --config.cwd=testDependencies' + }, + test1dot2: { + command: 'bower install angular#=1.2.18 angular-mocks#=1.2.18 angular-sanitize#=1.2.18 angular-ui-router#=0.2.15 --config.directory=. --config.cwd=testDependencies' + }, + testLatest: { + command: 'bower install angular angular-mocks angular-sanitize angular-ui-router --config.directory=. --config.cwd=testDependencies' + } + } + + }); + + // These plugins provide necessary tasks. + grunt.loadNpmTasks('grunt-bump'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-compress'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-conventional-changelog'); + grunt.loadNpmTasks('grunt-karma'); + grunt.loadNpmTasks('grunt-open'); + grunt.loadNpmTasks('grunt-shell'); + grunt.loadNpmTasks('grunt-text-replace'); + + grunt.registerTask('test', ['jshint', 'testMin', 'test1dot2', 'testLatest']); + grunt.registerTask('testMin', ['clean:test', 'shell:testMinimal', 'karma']); + grunt.registerTask('test1dot2', ['clean:test', 'shell:test1dot2', 'karma']); + grunt.registerTask('testLatest', ['clean:test', 'shell:testLatest', 'karma']); + + grunt.registerTask('default', ['test', 'concat:dev', 'uglify:dev']); + + grunt.registerTask('sample', ['concat:dev', 'copy:asset', 'copy:img', 'connect:livereload', 'open', 'watch']); + + grunt.registerTask('release-prepare', 'Update all files for a release', function(target) { + if(!target) { + target = 'patch'; + } + grunt.task.run( + 'bump-only:' + target, // Version update + 'test', // Tests + 'concat:release', // Concat with release banner + 'uglify:release', // Minify with release banner + 'changelog', // Changelog update + 'clean:release', // Delete old version download file + 'compress:release', // New version download file + 'replace:release' // Update version in download button (link & label) + ); + }); + +}; diff --git a/awx/ui/client/lib/angular-breadcrumb/LICENSE b/awx/ui/client/lib/angular-breadcrumb/LICENSE new file mode 100644 index 0000000000..f0774fcf13 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2013 Nicolas Cuillery + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/awx/ui/client/lib/angular-breadcrumb/bower.json b/awx/ui/client/lib/angular-breadcrumb/bower.json new file mode 100644 index 0000000000..3a0a2f944c --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/bower.json @@ -0,0 +1,33 @@ +{ + "name": "angular-breadcrumb", + "description": "AngularJS module that generates a breadcrumb from ui-router's states", + "version": "0.4.1", + "main": "release/angular-breadcrumb.js", + "ignore": [ + "sample", + "src", + "test", + ".bowerrc", + ".coveralls.yml", + ".gitignore", + ".jshintrc", + ".travis.yml", + "gruntfile.js", + "bower.json", + "karma.conf.js", + "libpeerconnection.log", + "package.json", + "README.md" + ], + "dependencies": { + "angular": ">=1.0.8", + "angular-ui-router": ">=0.2.0" + }, + "devDependencies": { + "bootstrap": "~2.3.2", + "angular-ui-bootstrap-bower": "~0.8.0", + "underscore": "~1.5.1", + "angular-mocks": ">=1.0.8", + "angular-sanitize": ">=1.0.8" + } +} diff --git a/awx/ui/client/lib/angular-breadcrumb/dist/angular-breadcrumb.js b/awx/ui/client/lib/angular-breadcrumb/dist/angular-breadcrumb.js new file mode 100644 index 0000000000..e20ca158ec --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/dist/angular-breadcrumb.js @@ -0,0 +1,369 @@ +/*! angular-breadcrumb - v0.4.0-dev-2015-08-07 +* http://ncuillery.github.io/angular-breadcrumb +* Copyright (c) 2015 Nicolas Cuillery; Licensed MIT */ + +(function (window, angular, undefined) { +'use strict'; + +function isAOlderThanB(scopeA, scopeB) { + if(angular.equals(scopeA.length, scopeB.length)) { + return scopeA > scopeB; + } else { + return scopeA.length > scopeB.length; + } +} + +function parseStateRef(ref) { + var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) { throw new Error("Invalid state ref '" + ref + "'"); } + return { state: parsed[1], paramExpr: parsed[3] || null }; +} + +function $Breadcrumb() { + + var $$options = { + prefixStateName: null, + template: 'bootstrap3', + templateUrl: null, + includeAbstract : false + }; + + this.setOptions = function(options) { + angular.extend($$options, options); + }; + + this.$get = ['$state', '$stateParams', '$rootScope', function($state, $stateParams, $rootScope) { + + var $lastViewScope = $rootScope; + + // Early catch of $viewContentLoaded event + $rootScope.$on('$viewContentLoaded', function (event) { + // With nested views, the event occur several times, in "wrong" order + if(!event.targetScope.ncyBreadcrumbIgnore && + isAOlderThanB(event.targetScope.$id, $lastViewScope.$id)) { + $lastViewScope = event.targetScope; + } + }); + + // Get the parent state + var $$parentState = function(state) { + // Check if state has explicit parent OR we try guess parent from its name + var parent = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1]; + var isObjectParent = typeof parent === "object"; + // if parent is a object reference, then extract the name + return isObjectParent ? parent.name : parent; + }; + + // Add the state in the chain if not already in and if not abstract + var $$addStateInChain = function(chain, stateRef) { + var conf, + parentParams, + ref = parseStateRef(stateRef), + force = false, + skip = false; + + for(var i=0, l=chain.length; i' + + '
  • ' + + '{{step.ncyBreadcrumbLabel}}' + + '{{step.ncyBreadcrumbLabel}}' + + '/' + + '
  • ' + + '', + bootstrap3: '' + }; + + return { + restrict: 'AE', + replace: true, + scope: {}, + template: $breadcrumb.getTemplate($$templates), + templateUrl: $breadcrumb.getTemplateUrl(), + link: { + post: function postLink(scope) { + var labelWatchers = []; + + var renderBreadcrumb = function() { + deregisterWatchers(labelWatchers); + labelWatchers = []; + + var viewScope = $breadcrumb.$getLastViewScope(); + scope.steps = $breadcrumb.getStatesChain(); + angular.forEach(scope.steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + step.ncyBreadcrumbLabel = parseLabel(viewScope); + // Watcher for further viewScope updates + registerWatchers(labelWatchers, parseLabel, viewScope, step); + } else { + step.ncyBreadcrumbLabel = step.name; + } + }); + }; + + $rootScope.$on('$viewContentLoaded', function (event) { + if(!event.targetScope.ncyBreadcrumbIgnore) { + renderBreadcrumb(); + } + }); + + // View(s) may be already loaded while the directive's linking + renderBreadcrumb(); + } + } + }; +} +BreadcrumbDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + +function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { + + return { + restrict: 'A', + scope: {}, + template: '{{ncyBreadcrumbLabel}}', + compile: function(cElement, cAttrs) { + + // Override the default template if ncyBreadcrumbLast has a value + var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbLast); + if(template) { + cElement.html(template); + } + + return { + post: function postLink(scope) { + var labelWatchers = []; + + var renderLabel = function() { + deregisterWatchers(labelWatchers); + labelWatchers = []; + + var viewScope = $breadcrumb.$getLastViewScope(); + var lastStep = $breadcrumb.getLastStep(); + if(lastStep) { + scope.ncyBreadcrumbLink = lastStep.ncyBreadcrumbLink; + if (lastStep.ncyBreadcrumb && lastStep.ncyBreadcrumb.label) { + var parseLabel = $interpolate(lastStep.ncyBreadcrumb.label); + scope.ncyBreadcrumbLabel = parseLabel(viewScope); + // Watcher for further viewScope updates + // Tricky last arg: the last step is the entire scope of the directive ! + registerWatchers(labelWatchers, parseLabel, viewScope, scope); + } else { + scope.ncyBreadcrumbLabel = lastStep.name; + } + } + }; + + $rootScope.$on('$viewContentLoaded', function (event) { + if(!event.targetScope.ncyBreadcrumbIgnore) { + renderLabel(); + } + }); + + // View(s) may be already loaded while the directive's linking + renderLabel(); + } + }; + + } + }; +} +BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + +function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { + + return { + restrict: 'A', + scope: {}, + template: '{{ncyBreadcrumbChain}}', + + compile: function(cElement, cAttrs) { + // Override the default template if ncyBreadcrumbText has a value + var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); + if(template) { + cElement.html(template); + } + + var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator) || ' / '; + + return { + post: function postLink(scope) { + var labelWatchers = []; + + var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope) { + angular.forEach(getExpression(interpolationFunction), function(expression) { + var watcher = viewScope.$watch(expression, function(newValue, oldValue) { + if (newValue !== oldValue) { + renderLabel(); + } + }); + labelWatcherArray.push(watcher); + }); + }; + + var renderLabel = function() { + deregisterWatchers(labelWatchers); + labelWatchers = []; + + var viewScope = $breadcrumb.$getLastViewScope(); + var steps = $breadcrumb.getStatesChain(); + var combinedLabels = []; + angular.forEach(steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + combinedLabels.push(parseLabel(viewScope)); + // Watcher for further viewScope updates + registerWatchersText(labelWatchers, parseLabel, viewScope); + } else { + combinedLabels.push(step.name); + } + }); + + scope.ncyBreadcrumbChain = combinedLabels.join(separator); + }; + + $rootScope.$on('$viewContentLoaded', function (event) { + if(!event.targetScope.ncyBreadcrumbIgnore) { + renderLabel(); + } + }); + + // View(s) may be already loaded while the directive's linking + renderLabel(); + } + }; + + } + }; +} +BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + +angular.module('ncy-angular-breadcrumb', ['ui.router.state']) + .provider('$breadcrumb', $Breadcrumb) + .directive('ncyBreadcrumb', BreadcrumbDirective) + .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) + .directive('ncyBreadcrumbText', BreadcrumbTextDirective); +})(window, window.angular); diff --git a/awx/ui/client/lib/angular-breadcrumb/dist/angular-breadcrumb.min.js b/awx/ui/client/lib/angular-breadcrumb/dist/angular-breadcrumb.min.js new file mode 100644 index 0000000000..1da045d879 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/dist/angular-breadcrumb.min.js @@ -0,0 +1,4 @@ +/*! angular-breadcrumb - v0.4.0-dev-2015-08-07 +* http://ncuillery.github.io/angular-breadcrumb +* Copyright (c) 2015 Nicolas Cuillery; Licensed MIT */ +!function(a,b,c){"use strict";function d(a,c){return b.equals(a.length,c.length)?a>c:a.length>c.length}function e(a){var b=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/);if(!b||4!==b.length)throw new Error("Invalid state ref '"+a+"'");return{state:b[1],paramExpr:b[3]||null}}function f(){var a={prefixStateName:null,template:"bootstrap3",templateUrl:null,includeAbstract:!1};this.setOptions=function(c){b.extend(a,c)},this.$get=["$state","$stateParams","$rootScope",function(b,f,g){var h=g;g.$on("$viewContentLoaded",function(a){!a.targetScope.ncyBreadcrumbIgnore&&d(a.targetScope.$id,h.$id)&&(h=a.targetScope)});var i=function(a){var b=a.parent||(/^(.+)\.[^.]+$/.exec(a.name)||[])[1],c="object"==typeof b;return c?b.name:b},j=function(c,d){for(var g,i,j=e(d),k=!1,l=!1,m=0,n=c.length;n>m;m+=1)if(c[m].name===j.state)return;g=b.get(j.state),g.ncyBreadcrumb&&(g.ncyBreadcrumb.force&&(k=!0),g.ncyBreadcrumb.skip&&(l=!0)),g["abstract"]&&!a.includeAbstract&&!k||l||(j.paramExpr&&(i=h.$eval(j.paramExpr)),g.ncyBreadcrumbLink=b.href(j.state,i||f||{}),c.unshift(g))},k=function(a){var c=e(a),d=b.get(c.state);if(d.ncyBreadcrumb&&d.ncyBreadcrumb.parent){var f="function"==typeof d.ncyBreadcrumb.parent,g=f?d.ncyBreadcrumb.parent(h):d.ncyBreadcrumb.parent;if(g)return g}return i(d)};return{getTemplate:function(b){return a.templateUrl?null:b[a.template]?b[a.template]:a.template},getTemplateUrl:function(){return a.templateUrl},getStatesChain:function(c){for(var d=[],e=b.$current.self.name;e;e=k(e))if(j(d,e),c&&d.length)return d;return a.prefixStateName&&j(d,a.prefixStateName),d},getLastStep:function(){var a=this.getStatesChain(!0);return a.length?a[0]:c},$getLastViewScope:function(){return h}}}]}function g(a,c,d){var e={bootstrap2:'',bootstrap3:''};return{restrict:"AE",replace:!0,scope:{},template:c.getTemplate(e),templateUrl:c.getTemplateUrl(),link:{post:function(e){var f=[],g=function(){l(f),f=[];var d=c.$getLastViewScope();e.steps=c.getStatesChain(),b.forEach(e.steps,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);b.ncyBreadcrumbLabel=c(d),k(f,c,d,b)}else b.ncyBreadcrumbLabel=b.name})};d.$on("$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||g()}),g()}}}}function h(a,b,c){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbLabel}}",compile:function(d,e){var f=d.attr(e.$attr.ncyBreadcrumbLast);return f&&d.html(f),{post:function(d){var e=[],f=function(){l(e),e=[];var c=b.$getLastViewScope(),f=b.getLastStep();if(f)if(d.ncyBreadcrumbLink=f.ncyBreadcrumbLink,f.ncyBreadcrumb&&f.ncyBreadcrumb.label){var g=a(f.ncyBreadcrumb.label);d.ncyBreadcrumbLabel=g(c),k(e,g,c,d)}else d.ncyBreadcrumbLabel=f.name};c.$on("$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||f()}),f()}}}}}function i(a,c,d){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbChain}}",compile:function(e,f){var g=e.attr(f.$attr.ncyBreadcrumbText);g&&e.html(g);var h=e.attr(f.$attr.ncyBreadcrumbTextSeparator)||" / ";return{post:function(e){var f=[],g=function(a,c,d){b.forEach(j(c),function(b){var c=d.$watch(b,function(a,b){a!==b&&i()});a.push(c)})},i=function(){l(f),f=[];var d=c.$getLastViewScope(),i=c.getStatesChain(),j=[];b.forEach(i,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);j.push(c(d)),g(f,c,d)}else j.push(b.name)}),e.ncyBreadcrumbChain=j.join(h)};d.$on("$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||i()}),i()}}}}}var j=function(a){if(a.expressions)return a.expressions;var c=[];return b.forEach(a.parts,function(a){b.isFunction(a)&&c.push(a.exp)}),c},k=function(a,c,d,e){b.forEach(j(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d)});a.push(f)})},l=function(a){b.forEach(a,function(a){a()})};g.$inject=["$interpolate","$breadcrumb","$rootScope"],h.$inject=["$interpolate","$breadcrumb","$rootScope"],i.$inject=["$interpolate","$breadcrumb","$rootScope"],b.module("ncy-angular-breadcrumb",["ui.router.state"]).provider("$breadcrumb",f).directive("ncyBreadcrumb",g).directive("ncyBreadcrumbLast",h).directive("ncyBreadcrumbText",i)}(window,window.angular); \ No newline at end of file diff --git a/awx/ui/client/lib/angular-breadcrumb/release/angular-breadcrumb.js b/awx/ui/client/lib/angular-breadcrumb/release/angular-breadcrumb.js new file mode 100644 index 0000000000..ba6e5dcd39 --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/release/angular-breadcrumb.js @@ -0,0 +1,369 @@ +/*! angular-breadcrumb - v0.4.1 +* http://ncuillery.github.io/angular-breadcrumb +* Copyright (c) 2015 Nicolas Cuillery; Licensed MIT */ + +(function (window, angular, undefined) { +'use strict'; + +function isAOlderThanB(scopeA, scopeB) { + if(angular.equals(scopeA.length, scopeB.length)) { + return scopeA > scopeB; + } else { + return scopeA.length > scopeB.length; + } +} + +function parseStateRef(ref) { + var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) { throw new Error("Invalid state ref '" + ref + "'"); } + return { state: parsed[1], paramExpr: parsed[3] || null }; +} + +function $Breadcrumb() { + + var $$options = { + prefixStateName: null, + template: 'bootstrap3', + templateUrl: null, + includeAbstract : false + }; + + this.setOptions = function(options) { + angular.extend($$options, options); + }; + + this.$get = ['$state', '$stateParams', '$rootScope', function($state, $stateParams, $rootScope) { + + var $lastViewScope = $rootScope; + + // Early catch of $viewContentLoaded event + $rootScope.$on('$viewContentLoaded', function (event) { + // With nested views, the event occur several times, in "wrong" order + if(!event.targetScope.ncyBreadcrumbIgnore && + isAOlderThanB(event.targetScope.$id, $lastViewScope.$id)) { + $lastViewScope = event.targetScope; + } + }); + + // Get the parent state + var $$parentState = function(state) { + // Check if state has explicit parent OR we try guess parent from its name + var parent = state.parent || (/^(.+)\.[^.]+$/.exec(state.name) || [])[1]; + var isObjectParent = typeof parent === "object"; + // if parent is a object reference, then extract the name + return isObjectParent ? parent.name : parent; + }; + + // Add the state in the chain if not already in and if not abstract + var $$addStateInChain = function(chain, stateRef) { + var conf, + parentParams, + ref = parseStateRef(stateRef), + force = false, + skip = false; + + for(var i=0, l=chain.length; i' + + '
  • ' + + '{{step.ncyBreadcrumbLabel}}' + + '{{step.ncyBreadcrumbLabel}}' + + '/' + + '
  • ' + + '', + bootstrap3: '' + }; + + return { + restrict: 'AE', + replace: true, + scope: {}, + template: $breadcrumb.getTemplate($$templates), + templateUrl: $breadcrumb.getTemplateUrl(), + link: { + post: function postLink(scope) { + var labelWatchers = []; + + var renderBreadcrumb = function() { + deregisterWatchers(labelWatchers); + labelWatchers = []; + + var viewScope = $breadcrumb.$getLastViewScope(); + scope.steps = $breadcrumb.getStatesChain(); + angular.forEach(scope.steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + step.ncyBreadcrumbLabel = parseLabel(viewScope); + // Watcher for further viewScope updates + registerWatchers(labelWatchers, parseLabel, viewScope, step); + } else { + step.ncyBreadcrumbLabel = step.name; + } + }); + }; + + $rootScope.$on('$viewContentLoaded', function (event) { + if(!event.targetScope.ncyBreadcrumbIgnore) { + renderBreadcrumb(); + } + }); + + // View(s) may be already loaded while the directive's linking + renderBreadcrumb(); + } + } + }; +} +BreadcrumbDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + +function BreadcrumbLastDirective($interpolate, $breadcrumb, $rootScope) { + + return { + restrict: 'A', + scope: {}, + template: '{{ncyBreadcrumbLabel}}', + compile: function(cElement, cAttrs) { + + // Override the default template if ncyBreadcrumbLast has a value + var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbLast); + if(template) { + cElement.html(template); + } + + return { + post: function postLink(scope) { + var labelWatchers = []; + + var renderLabel = function() { + deregisterWatchers(labelWatchers); + labelWatchers = []; + + var viewScope = $breadcrumb.$getLastViewScope(); + var lastStep = $breadcrumb.getLastStep(); + if(lastStep) { + scope.ncyBreadcrumbLink = lastStep.ncyBreadcrumbLink; + if (lastStep.ncyBreadcrumb && lastStep.ncyBreadcrumb.label) { + var parseLabel = $interpolate(lastStep.ncyBreadcrumb.label); + scope.ncyBreadcrumbLabel = parseLabel(viewScope); + // Watcher for further viewScope updates + // Tricky last arg: the last step is the entire scope of the directive ! + registerWatchers(labelWatchers, parseLabel, viewScope, scope); + } else { + scope.ncyBreadcrumbLabel = lastStep.name; + } + } + }; + + $rootScope.$on('$viewContentLoaded', function (event) { + if(!event.targetScope.ncyBreadcrumbIgnore) { + renderLabel(); + } + }); + + // View(s) may be already loaded while the directive's linking + renderLabel(); + } + }; + + } + }; +} +BreadcrumbLastDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + +function BreadcrumbTextDirective($interpolate, $breadcrumb, $rootScope) { + + return { + restrict: 'A', + scope: {}, + template: '{{ncyBreadcrumbChain}}', + + compile: function(cElement, cAttrs) { + // Override the default template if ncyBreadcrumbText has a value + var template = cElement.attr(cAttrs.$attr.ncyBreadcrumbText); + if(template) { + cElement.html(template); + } + + var separator = cElement.attr(cAttrs.$attr.ncyBreadcrumbTextSeparator) || ' / '; + + return { + post: function postLink(scope) { + var labelWatchers = []; + + var registerWatchersText = function(labelWatcherArray, interpolationFunction, viewScope) { + angular.forEach(getExpression(interpolationFunction), function(expression) { + var watcher = viewScope.$watch(expression, function(newValue, oldValue) { + if (newValue !== oldValue) { + renderLabel(); + } + }); + labelWatcherArray.push(watcher); + }); + }; + + var renderLabel = function() { + deregisterWatchers(labelWatchers); + labelWatchers = []; + + var viewScope = $breadcrumb.$getLastViewScope(); + var steps = $breadcrumb.getStatesChain(); + var combinedLabels = []; + angular.forEach(steps, function (step) { + if (step.ncyBreadcrumb && step.ncyBreadcrumb.label) { + var parseLabel = $interpolate(step.ncyBreadcrumb.label); + combinedLabels.push(parseLabel(viewScope)); + // Watcher for further viewScope updates + registerWatchersText(labelWatchers, parseLabel, viewScope); + } else { + combinedLabels.push(step.name); + } + }); + + scope.ncyBreadcrumbChain = combinedLabels.join(separator); + }; + + $rootScope.$on('$viewContentLoaded', function (event) { + if(!event.targetScope.ncyBreadcrumbIgnore) { + renderLabel(); + } + }); + + // View(s) may be already loaded while the directive's linking + renderLabel(); + } + }; + + } + }; +} +BreadcrumbTextDirective.$inject = ['$interpolate', '$breadcrumb', '$rootScope']; + +angular.module('ncy-angular-breadcrumb', ['ui.router.state']) + .provider('$breadcrumb', $Breadcrumb) + .directive('ncyBreadcrumb', BreadcrumbDirective) + .directive('ncyBreadcrumbLast', BreadcrumbLastDirective) + .directive('ncyBreadcrumbText', BreadcrumbTextDirective); +})(window, window.angular); diff --git a/awx/ui/client/lib/angular-breadcrumb/release/angular-breadcrumb.min.js b/awx/ui/client/lib/angular-breadcrumb/release/angular-breadcrumb.min.js new file mode 100644 index 0000000000..ceb49da17e --- /dev/null +++ b/awx/ui/client/lib/angular-breadcrumb/release/angular-breadcrumb.min.js @@ -0,0 +1,4 @@ +/*! angular-breadcrumb - v0.4.1 +* http://ncuillery.github.io/angular-breadcrumb +* Copyright (c) 2015 Nicolas Cuillery; Licensed MIT */ +!function(a,b,c){"use strict";function d(a,c){return b.equals(a.length,c.length)?a>c:a.length>c.length}function e(a){var b=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/);if(!b||4!==b.length)throw new Error("Invalid state ref '"+a+"'");return{state:b[1],paramExpr:b[3]||null}}function f(){var a={prefixStateName:null,template:"bootstrap3",templateUrl:null,includeAbstract:!1};this.setOptions=function(c){b.extend(a,c)},this.$get=["$state","$stateParams","$rootScope",function(b,f,g){var h=g;g.$on("$viewContentLoaded",function(a){!a.targetScope.ncyBreadcrumbIgnore&&d(a.targetScope.$id,h.$id)&&(h=a.targetScope)});var i=function(a){var b=a.parent||(/^(.+)\.[^.]+$/.exec(a.name)||[])[1],c="object"==typeof b;return c?b.name:b},j=function(c,d){for(var g,i,j=e(d),k=!1,l=!1,m=0,n=c.length;n>m;m+=1)if(c[m].name===j.state)return;g=b.get(j.state),g.ncyBreadcrumb&&(g.ncyBreadcrumb.force&&(k=!0),g.ncyBreadcrumb.skip&&(l=!0)),g["abstract"]&&!a.includeAbstract&&!k||l||(j.paramExpr&&(i=h.$eval(j.paramExpr)),g.ncyBreadcrumbLink=b.href(j.state,i||f||{}),c.unshift(g))},k=function(a){var c=e(a),d=b.get(c.state);if(d.ncyBreadcrumb&&d.ncyBreadcrumb.parent){var f="function"==typeof d.ncyBreadcrumb.parent,g=f?d.ncyBreadcrumb.parent(h):d.ncyBreadcrumb.parent;if(g)return g}return i(d)};return{getTemplate:function(b){return a.templateUrl?null:b[a.template]?b[a.template]:a.template},getTemplateUrl:function(){return a.templateUrl},getStatesChain:function(c){for(var d=[],e=b.$current.self.name;e;e=k(e))if(j(d,e),c&&d.length)return d;return a.prefixStateName&&j(d,a.prefixStateName),d},getLastStep:function(){var a=this.getStatesChain(!0);return a.length?a[0]:c},$getLastViewScope:function(){return h}}}]}function g(a,c,d){var e={bootstrap2:'',bootstrap3:''};return{restrict:"AE",replace:!0,scope:{},template:c.getTemplate(e),templateUrl:c.getTemplateUrl(),link:{post:function(e){var f=[],g=function(){l(f),f=[];var d=c.$getLastViewScope();e.steps=c.getStatesChain(),b.forEach(e.steps,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);b.ncyBreadcrumbLabel=c(d),k(f,c,d,b)}else b.ncyBreadcrumbLabel=b.name})};d.$on("$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||g()}),g()}}}}function h(a,b,c){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbLabel}}",compile:function(d,e){var f=d.attr(e.$attr.ncyBreadcrumbLast);return f&&d.html(f),{post:function(d){var e=[],f=function(){l(e),e=[];var c=b.$getLastViewScope(),f=b.getLastStep();if(f)if(d.ncyBreadcrumbLink=f.ncyBreadcrumbLink,f.ncyBreadcrumb&&f.ncyBreadcrumb.label){var g=a(f.ncyBreadcrumb.label);d.ncyBreadcrumbLabel=g(c),k(e,g,c,d)}else d.ncyBreadcrumbLabel=f.name};c.$on("$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||f()}),f()}}}}}function i(a,c,d){return{restrict:"A",scope:{},template:"{{ncyBreadcrumbChain}}",compile:function(e,f){var g=e.attr(f.$attr.ncyBreadcrumbText);g&&e.html(g);var h=e.attr(f.$attr.ncyBreadcrumbTextSeparator)||" / ";return{post:function(e){var f=[],g=function(a,c,d){b.forEach(j(c),function(b){var c=d.$watch(b,function(a,b){a!==b&&i()});a.push(c)})},i=function(){l(f),f=[];var d=c.$getLastViewScope(),i=c.getStatesChain(),j=[];b.forEach(i,function(b){if(b.ncyBreadcrumb&&b.ncyBreadcrumb.label){var c=a(b.ncyBreadcrumb.label);j.push(c(d)),g(f,c,d)}else j.push(b.name)}),e.ncyBreadcrumbChain=j.join(h)};d.$on("$viewContentLoaded",function(a){a.targetScope.ncyBreadcrumbIgnore||i()}),i()}}}}}var j=function(a){if(a.expressions)return a.expressions;var c=[];return b.forEach(a.parts,function(a){b.isFunction(a)&&c.push(a.exp)}),c},k=function(a,c,d,e){b.forEach(j(c),function(b){var f=d.$watch(b,function(){e.ncyBreadcrumbLabel=c(d)});a.push(f)})},l=function(a){b.forEach(a,function(a){a()})};g.$inject=["$interpolate","$breadcrumb","$rootScope"],h.$inject=["$interpolate","$breadcrumb","$rootScope"],i.$inject=["$interpolate","$breadcrumb","$rootScope"],b.module("ncy-angular-breadcrumb",["ui.router.state"]).provider("$breadcrumb",f).directive("ncyBreadcrumb",g).directive("ncyBreadcrumbLast",h).directive("ncyBreadcrumbText",i)}(window,window.angular); \ No newline at end of file diff --git a/awx/ui/client/lib/angular-ui-router/.bower.json b/awx/ui/client/lib/angular-ui-router/.bower.json new file mode 100644 index 0000000000..d4d0fa469d --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/.bower.json @@ -0,0 +1,34 @@ +{ + "name": "angular-ui-router", + "version": "0.2.15", + "main": "./release/angular-ui-router.js", + "dependencies": { + "angular": ">= 1.0.8" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "component.json", + "package.json", + "lib", + "config", + "sample", + "test", + "tests", + "ngdoc_assets", + "Gruntfile.js", + "files.js" + ], + "homepage": "https://github.com/angular-ui/ui-router", + "_release": "0.2.15", + "_resolution": { + "type": "version", + "tag": "0.2.15", + "commit": "805e69bae319e922e4d3265b7ef565058aaff850" + }, + "_source": "git://github.com/angular-ui/ui-router.git", + "_target": "~0.2.15", + "_originalSource": "angular-ui-router", + "_direct": true +} \ No newline at end of file diff --git a/awx/ui/client/lib/angular-ui-router/CHANGELOG.md b/awx/ui/client/lib/angular-ui-router/CHANGELOG.md new file mode 100644 index 0000000000..23b59d3a77 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/CHANGELOG.md @@ -0,0 +1,228 @@ + +### 0.2.14 (2015-04-23) + + +#### Bug Fixes + +* **$StateRefDirective:** resolve missing support for svg anchor elements #1667 ([0149a7bb](https://github.com/angular-ui/ui-router/commit/0149a7bb38b7af99388a1ad7cc9909a7b7c4439d)) +* **$urlMatcherFactory:** + * regex params should respect case-sensitivity ([1e10519f](https://github.com/angular-ui/ui-router/commit/1e10519f3be6bbf0cefdcce623cd2ade06e649e5), closes [#1671](https://github.com/angular-ui/ui-router/issues/1671)) + * unquote all dashes from array params ([06664d33](https://github.com/angular-ui/ui-router/commit/06664d330f882390655dcfa83e10276110d0d0fa)) + * add Type.$normalize function ([b0c6aa23](https://github.com/angular-ui/ui-router/commit/b0c6aa2350fdd3ce8483144774adc12f5a72b7e9)) + * make optional params regex grouping optional ([06f73794](https://github.com/angular-ui/ui-router/commit/06f737945e83e668d09cfc3bcffd04a500ff1963), closes [#1576](https://github.com/angular-ui/ui-router/issues/1576)) +* **$state:** allow about.*.** glob patterns ([e39b27a2](https://github.com/angular-ui/ui-router/commit/e39b27a2cb7d88525c446a041f9fbf1553202010)) +* **uiSref:** + * use Object's toString instead of Window's toString ([2aa7f4d1](https://github.com/angular-ui/ui-router/commit/2aa7f4d139dbd5b9fcc4afdcf2ab6642c87f5671)) + * add absolute to allowed transition options ([ae1b3c4e](https://github.com/angular-ui/ui-router/commit/ae1b3c4eedc37983400d830895afb50457c63af4)) +* **uiSrefActive:** Apply active classes on lazy loaded states ([f0ddbe7b](https://github.com/angular-ui/ui-router/commit/f0ddbe7b4a91daf279c3b7d0cee732bb1f3be5b4)) +* **uiView:** add `$element` to locals for view controller ([db68914c](https://github.com/angular-ui/ui-router/commit/db68914cd6c821e7dec8155bd33142a3a97f5453)) + + +#### Features + +* **$state:** + * support URLs with #fragments ([3da0a170](https://github.com/angular-ui/ui-router/commit/3da0a17069e27598c0f9d9164e104dd5ce05cdc6)) + * inject resolve params into controllerProvider ([b380c223](https://github.com/angular-ui/ui-router/commit/b380c223fe12e2fde7582c0d6b1ed7b15a23579b), closes [#1131](https://github.com/angular-ui/ui-router/issues/1131)) + * added 'state' to state reload method (feat no.1612) - modiefied options.reload ([b8f04575](https://github.com/angular-ui/ui-router/commit/b8f04575a8557035c1858c4d5c8dbde3e1855aaa)) + * broadcast $stateChangeCancel event when event.preventDefault() is called in $sta ([ecefb758](https://github.com/angular-ui/ui-router/commit/ecefb758cb445e41620b62a272aafa3638613d7a)) +* **$uiViewScroll:** change function to return promise ([c2a9a311](https://github.com/angular-ui/ui-router/commit/c2a9a311388bb212e5a2e820536d1d739f829ccd), closes [#1702](https://github.com/angular-ui/ui-router/issues/1702)) +* **uiSrefActive:** Added support for multiple nested uiSref directives ([b1844948](https://github.com/angular-ui/ui-router/commit/b18449481d152b50705abfce2493a444eb059fa5)) + + + +### 0.2.13 (2014-11-20) + +This release primarily fixes issues reported against 0.2.12 + +#### Bug Fixes + +* **$state:** fix $state.includes/.is to apply param types before comparisions fix(uiSref): ma ([19715d15](https://github.com/angular-ui/ui-router/commit/19715d15e3cbfff724519e9febedd05b49c75baa), closes [#1513](https://github.com/angular-ui/ui-router/issues/1513)) + * Avoid re-synchronizing from url after .transitionTo ([b267ecd3](https://github.com/angular-ui/ui-router/commit/b267ecd348e5c415233573ef95ebdbd051875f52), closes [#1573](https://github.com/angular-ui/ui-router/issues/1573)) +* **$urlMatcherFactory:** + * Built-in date type uses local time zone ([d726bedc](https://github.com/angular-ui/ui-router/commit/d726bedcbb5f70a5660addf43fd52ec730790293)) + * make date type fn check .is before running ([aa94ce3b](https://github.com/angular-ui/ui-router/commit/aa94ce3b86632ad05301530a2213099da73a3dc0), closes [#1564](https://github.com/angular-ui/ui-router/issues/1564)) + * early binding of array handler bypasses type resolution ([ada4bc27](https://github.com/angular-ui/ui-router/commit/ada4bc27df5eff3ba3ab0de94a09bd91b0f7a28c)) + * add 'any' Type for non-encoding non-url params ([3bfd75ab](https://github.com/angular-ui/ui-router/commit/3bfd75ab445ee2f1dd55275465059ed116b10b27), closes [#1562](https://github.com/angular-ui/ui-router/issues/1562)) + * fix encoding slashes in params ([0c983a08](https://github.com/angular-ui/ui-router/commit/0c983a08e2947f999683571477debd73038e95cf), closes [#1119](https://github.com/angular-ui/ui-router/issues/1119)) + * fix mixed path/query params ordering problem ([a479fbd0](https://github.com/angular-ui/ui-router/commit/a479fbd0b8eb393a94320973e5b9a62d83912ee2), closes [#1543](https://github.com/angular-ui/ui-router/issues/1543)) +* **ArrayType:** + * specify empty array mapping corner case ([74aa6091](https://github.com/angular-ui/ui-router/commit/74aa60917e996b0b4e27bbb4eb88c3c03832021d), closes [#1511](https://github.com/angular-ui/ui-router/issues/1511)) + * fix .equals for array types ([5e6783b7](https://github.com/angular-ui/ui-router/commit/5e6783b77af9a90ddff154f990b43dbb17eeda6e), closes [#1538](https://github.com/angular-ui/ui-router/issues/1538)) +* **Param:** fix default value shorthand declaration ([831d812a](https://github.com/angular-ui/ui-router/commit/831d812a524524c71f0ee1c9afaf0487a5a66230), closes [#1554](https://github.com/angular-ui/ui-router/issues/1554)) +* **common:** fixed the _.filter clone to not create sparse arrays ([750f5cf5](https://github.com/angular-ui/ui-router/commit/750f5cf5fd91f9ada96f39e50d39aceb2caf22b6), closes [#1563](https://github.com/angular-ui/ui-router/issues/1563)) +* **ie8:** fix calls to indexOf and filter ([dcb31b84](https://github.com/angular-ui/ui-router/commit/dcb31b843391b3e61dee4de13f368c109541813e), closes [#1556](https://github.com/angular-ui/ui-router/issues/1556)) + + +#### Features + +* add json parameter Type ([027f1fcf](https://github.com/angular-ui/ui-router/commit/027f1fcf9c0916cea651e88981345da6f9ff214a)) + + + +### 0.2.12 (2014-11-13) + +#### Bug Fixes + +* **$resolve:** use resolve fn result, not parent resolved value of same name ([67f5e00c](https://github.com/angular-ui/ui-router/commit/67f5e00cc9aa006ce3fe6cde9dff261c28eab70a), closes [#1317], [#1353]) +* **$state:** + * populate default params in .transitionTo. ([3f60fbe6](https://github.com/angular-ui/ui-router/commit/3f60fbe6d65ebeca8d97952c05aa1d269f1b7ba1), closes [#1396]) + * reload() now reinvokes controllers ([73443420](https://github.com/angular-ui/ui-router/commit/7344342018847902594dc1fc62d30a5c30f01763), closes [#582]) + * do not emit $viewContentLoading if notify: false ([74255feb](https://github.com/angular-ui/ui-router/commit/74255febdf48ae082a02ca1e735165f2c369a463), closes [#1387](https://github.com/angular-ui/ui-router/issues/1387)) + * register states at config-time ([4533fe36](https://github.com/angular-ui/ui-router/commit/4533fe36e0ab2f0143edd854a4145deaa013915a)) + * handle parent.name when parent is obj ([4533fe36](https://github.com/angular-ui/ui-router/commit/4533fe36e0ab2f0143edd854a4145deaa013915a)) +* **$urlMatcherFactory:** + * register types at config ([4533fe36](https://github.com/angular-ui/ui-router/commit/4533fe36e0ab2f0143edd854a4145deaa013915a), closes [#1476]) + * made path params default value "" for backwards compat ([8f998e71](https://github.com/angular-ui/ui-router/commit/8f998e71e43a0b31293331c981f5db0f0097b8ba)) + * Pre-replace certain param values for better mapping ([6374a3e2](https://github.com/angular-ui/ui-router/commit/6374a3e29ab932014a7c77d2e1ab884cc841a2e3)) + * fixed ParamSet.$$keys() ordering ([9136fecb](https://github.com/angular-ui/ui-router/commit/9136fecbc2bfd4fda748a9914f0225a46c933860)) + * empty string policy now respected in Param.value() ([db12c85c](https://github.com/angular-ui/ui-router/commit/db12c85c16f2d105415f9bbbdeb11863f64728e0)) + * "string" type now encodes/decodes slashes ([3045e415](https://github.com/angular-ui/ui-router/commit/3045e41577a8b8b8afc6039f42adddf5f3c061ec), closes [#1119]) + * allow arrays in both path and query params ([fdd2f2c1](https://github.com/angular-ui/ui-router/commit/fdd2f2c191c4a67c874fdb9ec9a34f8dde9ad180), closes [#1073], [#1045], [#1486], [#1394]) + * typed params in search ([8d4cab69](https://github.com/angular-ui/ui-router/commit/8d4cab69dd67058e1a716892cc37b7d80a57037f), closes [#1488](https://github.com/angular-ui/ui-router/issues/1488)) + * no longer generate unroutable urls ([cb9fd9d8](https://github.com/angular-ui/ui-router/commit/cb9fd9d8943cb26c7223f6990db29c82ae8740f8), closes [#1487](https://github.com/angular-ui/ui-router/issues/1487)) + * handle optional parameter followed by required parameter in url format. ([efc72106](https://github.com/angular-ui/ui-router/commit/efc72106ddcc4774b48ea176a505ef9e95193b41)) + * default to parameter string coersion. ([13a468a7](https://github.com/angular-ui/ui-router/commit/13a468a7d54c2fb0751b94c0c1841d580b71e6dc), closes [#1414](https://github.com/angular-ui/ui-router/issues/1414)) + * concat respects strictMode/caseInsensitive ([dd72e103](https://github.com/angular-ui/ui-router/commit/dd72e103edb342d9cf802816fe127e1bbd68fd5f), closes [#1395]) +* **ui-sref:** + * Allow sref state options to take a scope object ([b5f7b596](https://github.com/angular-ui/ui-router/commit/b5f7b59692ce4933e2d63eb5df3f50a4ba68ccc0)) + * replace raw href modification with attrs. ([08c96782](https://github.com/angular-ui/ui-router/commit/08c96782faf881b0c7ab00afc233ee6729548fa0)) + * nagivate to state when url is "" fix($state.href): generate href for state with ([656b5aab](https://github.com/angular-ui/ui-router/commit/656b5aab906e5749db9b5a080c6a83b95f50fd91), closes [#1363](https://github.com/angular-ui/ui-router/issues/1363)) + * Check that state is defined in isMatch() ([92aebc75](https://github.com/angular-ui/ui-router/commit/92aebc7520f88babdc6e266536086e07263514c3), closes [#1314](https://github.com/angular-ui/ui-router/issues/1314), [#1332](https://github.com/angular-ui/ui-router/issues/1332)) +* **uiView:** + * allow inteprolated ui-view names ([81f6a19a](https://github.com/angular-ui/ui-router/commit/81f6a19a432dac9198fd33243855bfd3b4fea8c0), closes [#1324](https://github.com/angular-ui/ui-router/issues/1324)) + * Made anim work with angular 1.3 ([c3bb7ad9](https://github.com/angular-ui/ui-router/commit/c3bb7ad903da1e1f3c91019cfd255be8489ff4ef), closes [#1367](https://github.com/angular-ui/ui-router/issues/1367), [#1345](https://github.com/angular-ui/ui-router/issues/1345)) +* **urlRouter:** html5Mode accepts an object from angular v1.3.0-rc.3 ([7fea1e9d](https://github.com/angular-ui/ui-router/commit/7fea1e9d0d8c6e09cc6c895ecb93d4221e9adf48)) +* **stateFilters:** mark state filters as stateful. ([a00b353e](https://github.com/angular-ui/ui-router/commit/a00b353e3036f64a81245c4e7898646ba218f833), closes [#1479]) +* **ui-router:** re-add IE8 compatibility for map/filter/keys ([8ce69d9f](https://github.com/angular-ui/ui-router/commit/8ce69d9f7c886888ab53eca7e53536f36b428aae), closes [#1518], [#1383]) +* **package:** point 'main' to a valid filename ([ac903350](https://github.com/angular-ui/ui-router/commit/ac9033501debb63364539d91fbf3a0cba4579f8e)) +* **travis:** make CI build faster ([0531de05](https://github.com/angular-ui/ui-router/commit/0531de052e414a8d839fbb4e7635e923e94865b3)) + + +#### Features + +##### Default and Typed params + +This release includes a lot of bug fixes around default/optional and typed parameters. As such, 0.2.12 is the first release where we recommend those features be used. + +* **$state:** + * add state params validation ([b1379e6a](https://github.com/angular-ui/ui-router/commit/b1379e6a4d38f7ed7436e05873932d7c279af578), closes [#1433](https://github.com/angular-ui/ui-router/issues/1433)) + * is/includes/get work on relative stateOrName ([232e94b3](https://github.com/angular-ui/ui-router/commit/232e94b3c2ca2c764bb9510046e4b61690c87852)) + * .reload() returns state transition promise ([639e0565](https://github.com/angular-ui/ui-router/commit/639e0565dece9d5544cc93b3eee6e11c99bd7373)) +* **$templateFactory:** request templateURL as text/html ([ccd60769](https://github.com/angular-ui/ui-router/commit/ccd6076904a4b801d77b47f6e2de4c06ce9962f8), closes [#1287]) +* **$urlMatcherFactory:** Made a Params and ParamSet class ([0cc1e6cc](https://github.com/angular-ui/ui-router/commit/0cc1e6cc461a4640618e2bb594566551c54834e2)) + + + + +### 0.2.11 (2014-08-26) + + +#### Bug Fixes + +* **$resolve:** Resolves only inherit from immediate parent fixes #702 ([df34e20c](https://github.com/angular-ui/ui-router/commit/df34e20c576299e7a3c8bd4ebc68d42341c0ace9)) +* **$state:** + * change $state.href default options.inherit to true ([deea695f](https://github.com/angular-ui/ui-router/commit/deea695f5cacc55de351ab985144fd233c02a769)) + * sanity-check state lookups ([456fd5ae](https://github.com/angular-ui/ui-router/commit/456fd5aec9ea507518927bfabd62b4afad4cf714), closes [#980](https://github.com/angular-ui/ui-router/issues/980)) + * didn't comply to inherit parameter ([09836781](https://github.com/angular-ui/ui-router/commit/09836781f126c1c485b06551eb9cfd4fa0f45c35)) + * allow view content loading broadcast ([7b78edee](https://github.com/angular-ui/ui-router/commit/7b78edeeb52a74abf4d3f00f79534033d5a08d1a)) +* **$urlMatcherFactory:** + * detect injected functions ([91f75ae6](https://github.com/angular-ui/ui-router/commit/91f75ae66c4d129f6f69e53bd547594e9661f5d5)) + * syntax ([1ebed370](https://github.com/angular-ui/ui-router/commit/1ebed37069bae8614d41541d56521f5c45f703f3)) +* **UrlMatcher:** + * query param function defaults ([f9c20530](https://github.com/angular-ui/ui-router/commit/f9c205304f10d8a4ebe7efe9025e642016479a51)) + * don't decode default values ([63607bdb](https://github.com/angular-ui/ui-router/commit/63607bdbbcb432d3fb37856a1cb3da0cd496804e)) +* **travis:** update Node version to fix build ([d6b95ef2](https://github.com/angular-ui/ui-router/commit/d6b95ef23d9dacb4eba08897f5190a0bcddb3a48)) +* **uiSref:** + * Generate an href for states with a blank url. closes #1293 ([691745b1](https://github.com/angular-ui/ui-router/commit/691745b12fa05d3700dd28f0c8d25f8a105074ad)) + * should inherit params by default ([b973dad1](https://github.com/angular-ui/ui-router/commit/b973dad155ad09a7975e1476bd096f7b2c758eeb)) + * cancel transition if preventDefault() has been called ([2e6d9167](https://github.com/angular-ui/ui-router/commit/2e6d9167d3afbfbca6427e53e012f94fb5fb8022)) +* **uiView:** Fixed infinite loop when is called .go() from a controller. ([e13988b8](https://github.com/angular-ui/ui-router/commit/e13988b8cd6231d75c78876ee9d012cc87f4a8d9), closes [#1194](https://github.com/angular-ui/ui-router/issues/1194)) +* **docs:** + * Fixed link to milestones ([6c0ae500](https://github.com/angular-ui/ui-router/commit/6c0ae500cc238ea9fc95adcc15415c55fc9e1f33)) + * fix bug in decorator example ([4bd00af5](https://github.com/angular-ui/ui-router/commit/4bd00af50b8b88a49d1545a76290731cb8e0feb1)) + * Removed an incorrect semi-colon ([af97cef8](https://github.com/angular-ui/ui-router/commit/af97cef8b967f2e32177e539ef41450dca131a7d)) + * Explain return value of rule as function ([5e887890](https://github.com/angular-ui/ui-router/commit/5e8878900a6ffe59a81aed531a3925e34a297377)) + + +#### Features + +* **$state:** + * allow parameters to pass unharmed ([8939d057](https://github.com/angular-ui/ui-router/commit/8939d0572ab1316e458ef016317ecff53131a822)) + * **BREAKING CHANGE**: state parameters are no longer automatically coerced to strings, and unspecified parameter values are now set to undefined rather than null. + * allow prevent syncUrl on failure ([753060b9](https://github.com/angular-ui/ui-router/commit/753060b910d5d2da600a6fa0757976e401c33172)) +* **typescript:** Add typescript definitions for component builds ([521ceb3f](https://github.com/angular-ui/ui-router/commit/521ceb3fd7850646422f411921e21ce5e7d82e0f)) +* **uiSref:** extend syntax for ui-sref ([71cad3d6](https://github.com/angular-ui/ui-router/commit/71cad3d636508b5a9fe004775ad1f1adc0c80c3e)) +* **uiSrefActive:** + * Also activate for child states. ([bf163ad6](https://github.com/angular-ui/ui-router/commit/bf163ad6ce176ce28792696c8302d7cdf5c05a01), closes [#818](https://github.com/angular-ui/ui-router/issues/818)) + * **BREAKING CHANGE** Since ui-sref-active now activates even when child states are active you may need to swap out your ui-sref-active with ui-sref-active-eq, thought typically we think devs want the auto inheritance. + + * uiSrefActiveEq: new directive with old ui-sref-active behavior +* **$urlRouter:** + * defer URL change interception ([c72d8ce1](https://github.com/angular-ui/ui-router/commit/c72d8ce11916d0ac22c81b409c9e61d7048554d7)) + * force URLs to have valid params ([d48505cd](https://github.com/angular-ui/ui-router/commit/d48505cd328d83e39d5706e085ba319715f999a6)) + * abstract $location handling ([08b4636b](https://github.com/angular-ui/ui-router/commit/08b4636b294611f08db35f00641eb5211686fb50)) +* **$urlMatcherFactory:** + * fail on bad parameters ([d8f124c1](https://github.com/angular-ui/ui-router/commit/d8f124c10d00c7e5dde88c602d966db261aea221)) + * date type support ([b7f074ff](https://github.com/angular-ui/ui-router/commit/b7f074ff65ca150a3cdbda4d5ad6cb17107300eb)) + * implement type support ([450b1f0e](https://github.com/angular-ui/ui-router/commit/450b1f0e8e03c738174ff967f688b9a6373290f4)) +* **UrlMatcher:** + * handle query string arrays ([9cf764ef](https://github.com/angular-ui/ui-router/commit/9cf764efab45fa9309368688d535ddf6e96d6449), closes [#373](https://github.com/angular-ui/ui-router/issues/373)) + * injectable functions as defaults ([00966ecd](https://github.com/angular-ui/ui-router/commit/00966ecd91fb745846039160cab707bfca8b3bec)) + * default values & type decoding for query params ([a472b301](https://github.com/angular-ui/ui-router/commit/a472b301389fbe84d1c1fa9f24852b492a569d11)) + * allow shorthand definitions ([5b724304](https://github.com/angular-ui/ui-router/commit/5b7243049793505e44b6608ea09878c37c95b1f5)) + * validates whole interface ([32b27db1](https://github.com/angular-ui/ui-router/commit/32b27db173722e9194ef1d5c0ea7d93f25a98d11)) + * implement non-strict matching ([a3e21366](https://github.com/angular-ui/ui-router/commit/a3e21366bee0475c9795a1ec76f70eec41c5b4e3)) + * add per-param config support ([07b3029f](https://github.com/angular-ui/ui-router/commit/07b3029f4d409cf955780113df92e36401b47580)) + * **BREAKING CHANGE**: the `params` option in state configurations must now be an object keyed by parameter name. + +### 0.2.10 (2014-03-12) + + +#### Bug Fixes + +* **$state:** use $browser.baseHref() when generating urls with .href() ([cbcc8488](https://github.com/angular-ui/ui-router/commit/cbcc84887d6b6d35258adabb97c714cd9c1e272d)) +* **bower.json:** JS files should not be ignored ([ccdab193](https://github.com/angular-ui/ui-router/commit/ccdab193315f304eb3be5f5b97c47a926c79263e)) +* **dev:** karma:background task is missing, can't run grunt:dev. ([d9f7b898](https://github.com/angular-ui/ui-router/commit/d9f7b898e8e3abb8c846b0faa16a382913d7b22b)) +* **sample:** Contacts menu button not staying active when navigating to detail states. Need t ([2fcb8443](https://github.com/angular-ui/ui-router/commit/2fcb84437cb43ade12682a92b764f13cac77dfe7)) +* **uiSref:** support mock-clicks/events with no data ([717d3ff7](https://github.com/angular-ui/ui-router/commit/717d3ff7d0ba72d239892dee562b401cdf90e418)) +* **uiView:** + * Do NOT autoscroll when autoscroll attr is missing ([affe5bd7](https://github.com/angular-ui/ui-router/commit/affe5bd785cdc3f02b7a9f64a52e3900386ec3a0), closes [#807](https://github.com/angular-ui/ui-router/issues/807)) + * Refactoring uiView directive to copy ngView logic ([548fab6a](https://github.com/angular-ui/ui-router/commit/548fab6ab9debc9904c5865c8bc68b4fc3271dd0), closes [#857](https://github.com/angular-ui/ui-router/issues/857), [#552](https://github.com/angular-ui/ui-router/issues/552)) + + +#### Features + +* **$state:** includes() allows glob patterns for state matching. ([2d5f6b37](https://github.com/angular-ui/ui-router/commit/2d5f6b37191a3135f4a6d9e8f344c54edcdc065b)) +* **UrlMatcher:** Add support for case insensitive url matching ([642d5247](https://github.com/angular-ui/ui-router/commit/642d524799f604811e680331002feec7199a1fb5)) +* **uiSref:** add support for transition options ([2ed7a728](https://github.com/angular-ui/ui-router/commit/2ed7a728cee6854b38501fbc1df6139d3de5b28a)) +* **uiView:** add controllerAs config with function ([1ee7334a](https://github.com/angular-ui/ui-router/commit/1ee7334a73efeccc9b95340e315cdfd59944762d)) + + +### 0.2.9 (2014-01-17) + + +This release is identical to 0.2.8. 0.2.8 was re-tagged in git to fix a problem with bower. + + +### 0.2.8 (2014-01-16) + + +#### Bug Fixes + +* **$state:** allow null to be passed as 'params' param ([094dc30e](https://github.com/angular-ui/ui-router/commit/094dc30e883e1bd14e50a475553bafeaade3b178)) +* **$state.go:** param inheritance shouldn't inherit from siblings ([aea872e0](https://github.com/angular-ui/ui-router/commit/aea872e0b983cb433436ce5875df10c838fccedb)) +* **bower.json:** fixes bower.json ([eed3cc4d](https://github.com/angular-ui/ui-router/commit/eed3cc4d4dfef1d3ef84b9fd063127538ebf59d3)) +* **uiSrefActive:** annotate controller injection ([85921422](https://github.com/angular-ui/ui-router/commit/85921422ff7fb0effed358136426d616cce3d583), closes [#671](https://github.com/angular-ui/ui-router/issues/671)) +* **uiView:** + * autoscroll tests pass on 1.2.4 & 1.1.5 ([86eacac0](https://github.com/angular-ui/ui-router/commit/86eacac09ca5e9000bd3b9c7ba6e2cc95d883a3a)) + * don't animate initial load ([83b6634d](https://github.com/angular-ui/ui-router/commit/83b6634d27942ca74766b2b1244a7fc52c5643d9)) + * test pass against 1.0.8 and 1.2.4 ([a402415a](https://github.com/angular-ui/ui-router/commit/a402415a2a28b360c43b9fe8f4f54c540f6c33de)) + * it should autoscroll when expr is missing. ([8bb9e27a](https://github.com/angular-ui/ui-router/commit/8bb9e27a2986725f45daf44c4c9f846385095aff)) + + +#### Features + +* **uiSref:** add target attribute behaviour ([c12bf9a5](https://github.com/angular-ui/ui-router/commit/c12bf9a520d30d70294e3d82de7661900f8e394e)) +* **uiView:** + * merge autoscroll expression test. ([b89e0f87](https://github.com/angular-ui/ui-router/commit/b89e0f871d5cc35c10925ede986c10684d5c9252)) + * cache and test autoscroll expression ([ee262282](https://github.com/angular-ui/ui-router/commit/ee2622828c2ce83807f006a459ac4e11406d9258)) diff --git a/awx/ui/client/lib/angular-ui-router/CONTRIBUTING.md b/awx/ui/client/lib/angular-ui-router/CONTRIBUTING.md new file mode 100644 index 0000000000..63829a5491 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/CONTRIBUTING.md @@ -0,0 +1,65 @@ + +# Report an Issue + +Help us make UI-Router better! If you think you might have found a bug, or some other weirdness, start by making sure +it hasn't already been reported. You can [search through existing issues](https://github.com/angular-ui/ui-router/search?q=wat%3F&type=Issues) +to see if someone's reported one similar to yours. + +If not, then [create a plunkr](http://bit.ly/UIR-Plunk) that demonstrates the problem (try to use as little code +as possible: the more minimalist, the faster we can debug it). + +Next, [create a new issue](https://github.com/angular-ui/ui-router/issues/new) that briefly explains the problem, +and provides a bit of background as to the circumstances that triggered it. Don't forget to include the link to +that plunkr you created! + +**Note**: If you're unsure how a feature is used, or are encountering some unexpected behavior that you aren't sure +is a bug, it's best to talk it out on +[StackOverflow](http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router) before reporting it. This +keeps development streamlined, and helps us focus on building great software. + + +Issues only! | +-------------| +Please keep in mind that the issue tracker is for *issues*. Please do *not* post an issue if you need help or support. Instead, see one of the above-mentioned forums or [IRC](irc://irc.freenode.net/#angularjs). | + +####Purple Labels +A purple label means that **you** need to take some further action. + - ![Not Actionable - Need Info](http://angular-ui.github.io/ui-router/images/notactionable.png): Your issue is not specific enough, or there is no clear action that we can take. Please clarify and refine your issue. + - ![Plunkr Please](http://angular-ui.github.io/ui-router/images/plunkrplease.png): Please [create a plunkr](http://bit.ly/UIR-Plunk) + - ![StackOverflow](http://angular-ui.github.io/ui-router/images/stackoverflow.png): We suspect your issue is really a help request, or could be answered by the community. Please ask your question on [StackOverflow](http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router). If you determine that is an actual issue, please explain why. + +If your issue gets labeled with purple label, no further action will be taken until you respond to the label appropriately. + +# Contribute + +**(1)** See the **[Developing](#developing)** section below, to get the development version of UI-Router up and running on your local machine. + +**(2)** Check out the [roadmap](https://github.com/angular-ui/ui-router/milestones) to see where the project is headed, and if your feature idea fits with where we're headed. + +**(3)** If you're not sure, [open an RFC](https://github.com/angular-ui/ui-router/issues/new?title=RFC:%20My%20idea) to get some feedback on your idea. + +**(4)** Finally, commit some code and open a pull request. Code & commits should abide by the following rules: + +- *Always* have test coverage for new features (or regression tests for bug fixes), and *never* break existing tests +- Commits should represent one logical change each; if a feature goes through multiple iterations, squash your commits down to one +- Make sure to follow the [Angular commit message format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) so your change will appear in the changelog of the next release. +- Changes should always respect the coding style of the project + + + +# Developing + +UI-Router uses grunt >= 0.4.x. Make sure to upgrade your environment and read the +[Migration Guide](http://gruntjs.com/upgrading-from-0.3-to-0.4). + +Dependencies for building from source and running tests: + +* [grunt-cli](https://github.com/gruntjs/grunt-cli) - run: `$ npm install -g grunt-cli` +* Then, install the development dependencies by running `$ npm install` from the project directory + +There are a number of targets in the gruntfile that are used to generating different builds: + +* `grunt`: Perform a normal build, runs jshint and karma tests +* `grunt build`: Perform a normal build +* `grunt dist`: Perform a clean build and generate documentation +* `grunt dev`: Run dev server (sample app) and watch for changes, builds and runs karma tests on changes. diff --git a/awx/ui/client/lib/angular-ui-router/LICENSE b/awx/ui/client/lib/angular-ui-router/LICENSE new file mode 100644 index 0000000000..6413b092d7 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2013-2015 The AngularUI Team, Karsten Sperling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/awx/ui/client/lib/angular-ui-router/README.md b/awx/ui/client/lib/angular-ui-router/README.md new file mode 100644 index 0000000000..1d8bcd6180 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/README.md @@ -0,0 +1,245 @@ +# AngularUI Router  [![Build Status](https://travis-ci.org/angular-ui/ui-router.svg?branch=master)](https://travis-ci.org/angular-ui/ui-router) + +#### The de-facto solution to flexible routing with nested views +--- +**[Download 0.2.15](http://angular-ui.github.io/ui-router/release/angular-ui-router.js)** (or **[Minified](http://angular-ui.github.io/ui-router/release/angular-ui-router.min.js)**) **|** +**[Guide](https://github.com/angular-ui/ui-router/wiki) |** +**[API](http://angular-ui.github.io/ui-router/site) |** +**[Sample](http://angular-ui.github.com/ui-router/sample/) ([Src](https://github.com/angular-ui/ui-router/tree/gh-pages/sample)) |** +**[FAQ](https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions) |** +**[Resources](#resources) |** +**[Report an Issue](https://github.com/angular-ui/ui-router/blob/master/CONTRIBUTING.md#report-an-issue) |** +**[Contribute](https://github.com/angular-ui/ui-router/blob/master/CONTRIBUTING.md#contribute) |** +**[Help!](http://stackoverflow.com/questions/ask?tags=angularjs,angular-ui-router) |** +**[Discuss](https://groups.google.com/forum/#!categories/angular-ui/router)** + +--- + +AngularUI Router is a routing framework for [AngularJS](http://angularjs.org), which allows you to organize the +parts of your interface into a [*state machine*](https://en.wikipedia.org/wiki/Finite-state_machine). Unlike the +[`$route` service](http://docs.angularjs.org/api/ngRoute.$route) in the Angular ngRoute module, which is organized around URL +routes, UI-Router is organized around [*states*](https://github.com/angular-ui/ui-router/wiki), +which may optionally have routes, as well as other behavior, attached. + +States are bound to *named*, *nested* and *parallel views*, allowing you to powerfully manage your application's interface. + +Check out the sample app: http://angular-ui.github.io/ui-router/sample/ + +- +**Note:** *UI-Router is under active development. As such, while this library is well-tested, the API may change. Consider using it in production applications only if you're comfortable following a changelog and updating your usage accordingly.* + + +## Get Started + +**(1)** Get UI-Router in one of the following ways: + - clone & [build](CONTRIBUTING.md#developing) this repository + - [download the release](http://angular-ui.github.io/ui-router/release/angular-ui-router.js) (or [minified](http://angular-ui.github.io/ui-router/release/angular-ui-router.min.js)) + - [link to cdn](http://cdnjs.com/libraries/angular-ui-router) + - via **[jspm](http://jspm.io/)**: by running `$ jspm install angular-ui-router` from your console + - or via **[npm](https://www.npmjs.org/)**: by running `$ npm install angular-ui-router` from your console + - or via **[Bower](http://bower.io/)**: by running `$ bower install angular-ui-router` from your console + - or via **[Component](https://github.com/component/component)**: by running `$ component install angular-ui/ui-router` from your console + +**(2)** Include `angular-ui-router.js` (or `angular-ui-router.min.js`) in your `index.html`, after including Angular itself (For Component users: ignore this step) + +**(3)** Add `'ui.router'` to your main module's list of dependencies (For Component users: replace `'ui.router'` with `require('angular-ui-router')`) + +When you're done, your setup should look similar to the following: + +> +```html + + + + + + + ... + + + ... + + +``` + +### [Nested States & Views](http://plnkr.co/edit/u18KQc?p=preview) + +The majority of UI-Router's power is in its ability to nest states & views. + +**(1)** First, follow the [setup](#get-started) instructions detailed above. + +**(2)** Then, add a [`ui-view` directive](https://github.com/angular-ui/ui-router/wiki/Quick-Reference#ui-view) to the `` of your app. + +> +```html + + +
    + + State 1 + State 2 + +``` + +**(3)** You'll notice we also added some links with [`ui-sref` directives](https://github.com/angular-ui/ui-router/wiki/Quick-Reference#ui-sref). In addition to managing state transitions, this directive auto-generates the `href` attribute of the `` element it's attached to, if the corresponding state has a URL. Next we'll add some templates. These will plug into the `ui-view` within `index.html`. Notice that they have their own `ui-view` as well! That is the key to nesting states and views. + +> +```html + +

    State 1

    +
    +
    Show List +
    +``` +```html + +

    State 2

    +
    +Show List +
    +``` + +**(4)** Next, we'll add some child templates. *These* will get plugged into the `ui-view` of their parent state templates. + +> +```html + +

    List of State 1 Items

    +
      +
    • {{ item }}
    • +
    +``` + +> +```html + +

    List of State 2 Things

    +
      +
    • {{ thing }}
    • +
    +``` + +**(5)** Finally, we'll wire it all up with `$stateProvider`. Set up your states in the module config, as in the following: + + +> +```javascript +myApp.config(function($stateProvider, $urlRouterProvider) { + // + // For any unmatched url, redirect to /state1 + $urlRouterProvider.otherwise("/state1"); + // + // Now set up the states + $stateProvider + .state('state1', { + url: "/state1", + templateUrl: "partials/state1.html" + }) + .state('state1.list', { + url: "/list", + templateUrl: "partials/state1.list.html", + controller: function($scope) { + $scope.items = ["A", "List", "Of", "Items"]; + } + }) + .state('state2', { + url: "/state2", + templateUrl: "partials/state2.html" + }) + .state('state2.list', { + url: "/list", + templateUrl: "partials/state2.list.html", + controller: function($scope) { + $scope.things = ["A", "Set", "Of", "Things"]; + } + }); +}); +``` + +**(6)** See this quick start example in action. +>**[Go to Quick Start Plunker for Nested States & Views](http://plnkr.co/edit/u18KQc?p=preview)** + +**(7)** This only scratches the surface +>**[Dive Deeper!](https://github.com/angular-ui/ui-router/wiki)** + + +### [Multiple & Named Views](http://plnkr.co/edit/SDOcGS?p=preview) + +Another great feature is the ability to have multiple `ui-view`s view per template. + +**Pro Tip:** *While multiple parallel views are a powerful feature, you'll often be able to manage your +interfaces more effectively by nesting your views, and pairing those views with nested states.* + +**(1)** Follow the [setup](#get-started) instructions detailed above. + +**(2)** Add one or more `ui-view` to your app, give them names. +> +```html + + +
    +
    + + Route 1 + Route 2 + +``` + +**(3)** Set up your states in the module config: +> +```javascript +myApp.config(function($stateProvider) { + $stateProvider + .state('index', { + url: "", + views: { + "viewA": { template: "index.viewA" }, + "viewB": { template: "index.viewB" } + } + }) + .state('route1', { + url: "/route1", + views: { + "viewA": { template: "route1.viewA" }, + "viewB": { template: "route1.viewB" } + } + }) + .state('route2', { + url: "/route2", + views: { + "viewA": { template: "route2.viewA" }, + "viewB": { template: "route2.viewB" } + } + }) +}); +``` + +**(4)** See this quick start example in action. +>**[Go to Quick Start Plunker for Multiple & Named Views](http://plnkr.co/edit/SDOcGS?p=preview)** + + +## Resources + +* [In-Depth Guide](https://github.com/angular-ui/ui-router/wiki) +* [API Reference](http://angular-ui.github.io/ui-router/site) +* [Sample App](http://angular-ui.github.com/ui-router/sample/) ([Source](https://github.com/angular-ui/ui-router/tree/gh-pages/sample)) +* [FAQ](https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions) +* [Slides comparing ngRoute to ui-router](http://slid.es/timkindberg/ui-router#/) +* [UI-Router Extras / Addons](http://christopherthielen.github.io/ui-router-extras/#/home) (@christopherthielen) + +### Videos + +* [Introduction Video](https://egghead.io/lessons/angularjs-introduction-ui-router) (egghead.io) +* [Tim Kindberg on Angular UI-Router](https://www.youtube.com/watch?v=lBqiZSemrqg) +* [Activating States](https://egghead.io/lessons/angularjs-ui-router-activating-states) (egghead.io) +* [Learn Angular.js using UI-Router](http://youtu.be/QETUuZ27N0w) (LearnCode.academy) + + + +## Reporting issues and Contributing + +Please read our [Contributor guidelines](CONTRIBUTING.md) before reporting an issue or creating a pull request. diff --git a/awx/ui/client/lib/angular-ui-router/api/angular-ui-router.d.ts b/awx/ui/client/lib/angular-ui-router/api/angular-ui-router.d.ts new file mode 100644 index 0000000000..55c5d5e07f --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/api/angular-ui-router.d.ts @@ -0,0 +1,126 @@ +// Type definitions for Angular JS 1.1.5+ (ui.router module) +// Project: https://github.com/angular-ui/ui-router +// Definitions by: Michel Salib +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +declare module ng.ui { + + interface IState { + name?: string; + template?: string; + templateUrl?: any; // string || () => string + templateProvider?: any; // () => string || IPromise + controller?: any; + controllerAs?: string; + controllerProvider?: any; + resolve?: {}; + url?: string; + params?: any; + views?: {}; + abstract?: boolean; + onEnter?: (...args: any[]) => void; + onExit?: (...args: any[]) => void; + data?: any; + reloadOnSearch?: boolean; + } + + interface ITypedState extends IState { + data?: T; + } + + interface IStateProvider extends IServiceProvider { + state(name: string, config: IState): IStateProvider; + state(config: IState): IStateProvider; + decorator(name?: string, decorator?: (state: IState, parent: Function) => any): any; + } + + interface IUrlMatcher { + concat(pattern: string): IUrlMatcher; + exec(path: string, searchParams: {}): {}; + parameters(): string[]; + format(values: {}): string; + } + + interface IUrlMatcherFactory { + compile(pattern: string): IUrlMatcher; + isMatcher(o: any): boolean; + } + + interface IUrlRouterProvider extends IServiceProvider { + when(whenPath: RegExp, handler: Function): IUrlRouterProvider; + when(whenPath: RegExp, handler: any[]): IUrlRouterProvider; + when(whenPath: RegExp, toPath: string): IUrlRouterProvider; + when(whenPath: IUrlMatcher, hanlder: Function): IUrlRouterProvider; + when(whenPath: IUrlMatcher, handler: any[]): IUrlRouterProvider; + when(whenPath: IUrlMatcher, toPath: string): IUrlRouterProvider; + when(whenPath: string, handler: Function): IUrlRouterProvider; + when(whenPath: string, handler: any[]): IUrlRouterProvider; + when(whenPath: string, toPath: string): IUrlRouterProvider; + otherwise(handler: Function): IUrlRouterProvider; + otherwise(handler: any[]): IUrlRouterProvider; + otherwise(path: string): IUrlRouterProvider; + rule(handler: Function): IUrlRouterProvider; + rule(handler: any[]): IUrlRouterProvider; + } + + interface IStateOptions { + location?: any; + inherit?: boolean; + relative?: IState; + notify?: boolean; + reload?: boolean; + } + + interface IHrefOptions { + lossy?: boolean; + inherit?: boolean; + relative?: IState; + absolute?: boolean; + } + + interface IStateService { + go(to: string, params?: {}, options?: IStateOptions): IPromise; + transitionTo(state: string, params?: {}, updateLocation?: boolean): void; + transitionTo(state: string, params?: {}, options?: IStateOptions): void; + includes(state: string, params?: {}): boolean; + is(state:string, params?: {}): boolean; + is(state: IState, params?: {}): boolean; + href(state: IState, params?: {}, options?: IHrefOptions): string; + href(state: string, params?: {}, options?: IHrefOptions): string; + get(state: string): IState; + get(): IState[]; + current: IState; + params: any; + reload(): void; + } + + interface IStateParamsService { + [key: string]: any; + } + + interface IStateParams { + [key: string]: any; + } + + interface IUrlRouterService { + /* + * Triggers an update; the same update that happens when the address bar + * url changes, aka $locationChangeSuccess. + * + * This method is useful when you need to use preventDefault() on the + * $locationChangeSuccess event, perform some custom logic (route protection, + * auth, config, redirection, etc) and then finally proceed with the transition + * by calling $urlRouter.sync(). + * + */ + sync(): void; + } + + interface IUiViewScrollProvider { + /* + * Reverts back to using the core $anchorScroll service for scrolling + * based on the url anchor. + */ + useAnchorScroll(): void; + } +} diff --git a/awx/ui/client/lib/angular-ui-router/bower.json b/awx/ui/client/lib/angular-ui-router/bower.json new file mode 100644 index 0000000000..f4f08fa265 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/bower.json @@ -0,0 +1,23 @@ +{ + "name": "angular-ui-router", + "version": "0.2.15", + "main": "./release/angular-ui-router.js", + "dependencies": { + "angular": ">= 1.0.8" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "component.json", + "package.json", + "lib", + "config", + "sample", + "test", + "tests", + "ngdoc_assets", + "Gruntfile.js", + "files.js" + ] +} diff --git a/awx/ui/client/lib/angular-ui-router/release/angular-ui-router.js b/awx/ui/client/lib/angular-ui-router/release/angular-ui-router.js new file mode 100644 index 0000000000..57c62cca88 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/release/angular-ui-router.js @@ -0,0 +1,4370 @@ +/** + * State-based routing for AngularJS + * @version v0.2.15 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + +/* commonjs package manager support (eg componentjs) */ +if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = 'ui.router'; +} + +(function (window, angular, undefined) { +/*jshint globalstrict:true*/ +/*global angular:false*/ +'use strict'; + +var isDefined = angular.isDefined, + isFunction = angular.isFunction, + isString = angular.isString, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy; + +function inherit(parent, extra) { + return extend(new (extend(function() {}, { prototype: parent }))(), extra); +} + +function merge(dst) { + forEach(arguments, function(obj) { + if (obj !== dst) { + forEach(obj, function(value, key) { + if (!dst.hasOwnProperty(key)) dst[key] = value; + }); + } + }); + return dst; +} + +/** + * Finds the common ancestor path between two states. + * + * @param {Object} first The first state. + * @param {Object} second The second state. + * @return {Array} Returns an array of state names in descending order, not including the root. + */ +function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; +} + +/** + * IE8-safe wrapper for `Object.keys()`. + * + * @param {Object} object A JavaScript object. + * @return {Array} Returns the keys of the object as an array. + */ +function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + forEach(object, function(val, key) { + result.push(key); + }); + return result; +} + +/** + * IE8-safe wrapper for `Array.prototype.indexOf()`. + * + * @param {Array} array A JavaScript array. + * @param {*} value A value to search the array for. + * @return {Number} Returns the array index value of `value`, or `-1` if not present. + */ +function indexOf(array, value) { + if (Array.prototype.indexOf) { + return array.indexOf(value, Number(arguments[2]) || 0); + } + var len = array.length >>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; +} + +/** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @param {Object} currentParams The value of the current state parameters ($stateParams). + * @param {Object} newParams The set of parameters which will be composited with inherited params. + * @param {Object} $current Internal definition of object representing the current state. + * @param {Object} $to Internal definition of object representing state to transition to. + */ +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i].params) continue; + parentParams = objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (indexOf(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +/** + * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. + * + * @param {Object} a The first object. + * @param {Object} b The second object. + * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, + * it defaults to the list of keys in `a`. + * @return {Boolean} Returns `true` if the keys match, otherwise `false`. + */ +function equalForKeys(a, b, keys) { + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i=0; i + * + * + * + * + * + * + * + * + * + * + * + * + */ +angular.module('ui.router', ['ui.router.state']); + +angular.module('ui.router.compat', ['ui.router']); + +/** + * @ngdoc object + * @name ui.router.util.$resolve + * + * @requires $q + * @requires $injector + * + * @description + * Manages resolution of (acyclic) graphs of promises. + */ +$Resolve.$inject = ['$q', '$injector']; +function $Resolve( $q, $injector) { + + var VISIT_IN_PROGRESS = 1, + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + + + /** + * @ngdoc function + * @name ui.router.util.$resolve#study + * @methodOf ui.router.util.$resolve + * + * @description + * Studies a set of invocables that are likely to be used multiple times. + *
    +   * $resolve.study(invocables)(locals, parent, self)
    +   * 
    + * is equivalent to + *
    +   * $resolve.resolve(invocables, locals, parent, self)
    +   * 
    + * but the former is more efficient (in fact `resolve` just calls `study` + * internally). + * + * @param {object} invocables Invocable objects + * @return {function} a function to pass in locals, parent and self + */ + this.study = function (invocables) { + if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + var invocableKeys = objectKeys(invocables || {}); + + // Perform a topological sort of invocables to build an ordered plan + var plan = [], cycle = [], visited = {}; + function visit(value, key) { + if (visited[key] === VISIT_DONE) return; + + cycle.push(key); + if (visited[key] === VISIT_IN_PROGRESS) { + cycle.splice(0, indexOf(cycle, key)); + throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + } + visited[key] = VISIT_IN_PROGRESS; + + if (isString(value)) { + plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); + } else { + var params = $injector.annotate(value); + forEach(params, function (param) { + if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); + }); + plan.push(key, value, params); + } + + cycle.pop(); + visited[key] = VISIT_DONE; + } + forEach(invocables, visit); + invocables = cycle = visited = null; // plan is all that's required + + function isResolve(value) { + return isObject(value) && value.then && value.$$promises; + } + + return function (locals, parent, self) { + if (isResolve(locals) && self === undefined) { + self = parent; parent = locals; locals = null; + } + if (!locals) locals = NO_LOCALS; + else if (!isObject(locals)) { + throw new Error("'locals' must be an object"); + } + if (!parent) parent = NO_PARENT; + else if (!isResolve(parent)) { + throw new Error("'parent' must be a promise returned by $resolve.resolve()"); + } + + // To complete the overall resolution, we have to wait for the parent + // promise and for the promise for each invokable in our plan. + var resolution = $q.defer(), + result = resolution.promise, + promises = result.$$promises = {}, + values = extend({}, locals), + wait = 1 + plan.length/3, + merged = false; + + function done() { + // Merge parent values we haven't got yet and publish our own $$values + if (!--wait) { + if (!merged) merge(values, parent.$$values); + result.$$values = values; + result.$$promises = result.$$promises || true; // keep for isResolve() + delete result.$$inheritedValues; + resolution.resolve(values); + } + } + + function fail(reason) { + result.$$failure = reason; + resolution.reject(reason); + } + + // Short-circuit if parent has already failed + if (isDefined(parent.$$failure)) { + fail(parent.$$failure); + return result; + } + + if (parent.$$inheritedValues) { + merge(values, omit(parent.$$inheritedValues, invocableKeys)); + } + + // Merge parent values if the parent has already resolved, or merge + // parent promises and wait if the parent resolve is still in progress. + extend(promises, parent.$$promises); + if (parent.$$values) { + merged = merge(values, omit(parent.$$values, invocableKeys)); + result.$$inheritedValues = omit(parent.$$values, invocableKeys); + done(); + } else { + if (parent.$$inheritedValues) { + result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); + } + parent.then(done, fail); + } + + // Process each invocable in the plan, but ignore any where a local of the same name exists. + for (var i=0, ii=plan.length; i} The template html as a string, or a promise + * for that string. + */ + this.fromUrl = function (url, params) { + if (isFunction(url)) url = url(params); + if (url == null) return null; + else return $http + .get(url, { cache: $templateCache, headers: { Accept: 'text/html' }}) + .then(function(response) { return response.data; }); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromProvider + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template by invoking an injectable provider function. + * + * @param {Function} provider Function to invoke via `$injector.invoke` + * @param {Object} params Parameters for the template. + * @param {Object} locals Locals to pass to `invoke`. Defaults to + * `{ params: params }`. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + this.fromProvider = function (provider, params, locals) { + return $injector.invoke(provider, null, locals || { params: params }); + }; +} + +angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); + +var $$UMFP; // reference to $UrlMatcherFactoryProvider + +/** + * @ngdoc object + * @name ui.router.util.type:UrlMatcher + * + * @description + * Matches URLs against patterns and extracts named parameters from the path or the search + * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list + * of search parameters. Multiple search parameter names are separated by '&'. Search parameters + * do not influence whether or not a URL is matched, but their values are passed through into + * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. + * + * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace + * syntax, which optionally allows a regular expression for the parameter to be specified: + * + * * `':'` name - colon placeholder + * * `'*'` name - catch-all placeholder + * * `'{' name '}'` - curly placeholder + * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the + * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. + * + * Parameter names may contain only word characters (latin letters, digits, and underscore) and + * must be unique within the pattern (across both path and search parameters). For colon + * placeholders or curly placeholders without an explicit regexp, a path parameter matches any + * number of characters other than '/'. For catch-all placeholders the path parameter matches + * any number of characters. + * + * Examples: + * + * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for + * trailing slashes, and patterns have to match the entire path, not just a prefix. + * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or + * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. + * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. + * * `'/user/{id:[^/]*}'` - Same as the previous example. + * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id + * parameter consists of 1 to 8 hex digits. + * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the + * path into the parameter 'path'. + * * `'/files/*path'` - ditto. + * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined + * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start + * + * @param {string} pattern The pattern to compile into a matcher. + * @param {Object} config A configuration object hash: + * @param {Object=} parentMatcher Used to concatenate the pattern/config onto + * an existing UrlMatcher + * + * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. + * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. + * + * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any + * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns + * non-null) will start with this prefix. + * + * @property {string} source The pattern that was passed into the constructor + * + * @property {string} sourcePath The path portion of the source property + * + * @property {string} sourceSearch The search portion of the source property + * + * @property {string} regex The constructed regex that will be used to match against the url when + * it is time to determine which url will match. + * + * @returns {Object} New `UrlMatcher` object + */ +function UrlMatcher(pattern, config, parentMatcher) { + config = extend({ params: {} }, isObject(config) ? config : {}); + + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: + // '*' name + // ':' name + // '{' name '}' + // '{' name ':' regexp '}' + // The regular expression is somewhat complicated due to the need to allow curly braces + // inside the regular expression. The placeholder regexp breaks down as follows: + // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) + // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case + // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + compiled = '^', last = 0, m, + segments = this.segments = [], + parentParams = parentMatcher ? parentMatcher.params : {}, + params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), + paramNames = []; + + function addParameter(id, type, config, location) { + paramNames.push(id); + if (parentParams[id]) return parentParams[id]; + if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + params[id] = new $$UMFP.Param(id, type, config, location); + return params[id]; + } + + function quoteRegExp(string, pattern, squash, optional) { + var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + switch(squash) { + case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; + case true: surroundPattern = ['?(', ')?']; break; + default: surroundPattern = ['(' + squash + "|", ')?']; break; + } + return result + surroundPattern[0] + pattern + surroundPattern[1]; + } + + this.source = pattern; + + // Split into static segments separated by path parameter placeholders. + // The number of segments is always 1 more than the number of parameters. + function matchDetails(m, isSearch) { + var id, regexp, segment, type, cfg, arrayMode; + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + cfg = config.params[id]; + segment = pattern.substring(last, m.index); + regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); + type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + return { + id: id, regexp: regexp, segment: segment, type: type, cfg: cfg + }; + } + + var p, param, segment; + while ((m = placeholder.exec(pattern))) { + p = matchDetails(m, false); + if (p.segment.indexOf('?') >= 0) break; // we're into the search part + + param = addParameter(p.id, p.type, p.cfg, "path"); + compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); + segments.push(p.segment); + last = placeholder.lastIndex; + } + segment = pattern.substring(last); + + // Find any search parameter names and remove them from the last segment + var i = segment.indexOf('?'); + + if (i >= 0) { + var search = this.sourceSearch = segment.substring(i); + segment = segment.substring(0, i); + this.sourcePath = pattern.substring(0, last + i); + + if (search.length > 0) { + last = 0; + while ((m = searchPlaceholder.exec(search))) { + p = matchDetails(m, true); + param = addParameter(p.id, p.type, p.cfg, "search"); + last = placeholder.lastIndex; + // check if ?& + } + } + } else { + this.sourcePath = pattern; + this.sourceSearch = ''; + } + + compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; + segments.push(segment); + + this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); + this.prefix = segments[0]; + this.$$paramNames = paramNames; +} + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#concat + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns a new matcher for a pattern constructed by appending the path part and adding the + * search parameters of the specified pattern to this pattern. The current pattern is not + * modified. This can be understood as creating a pattern for URLs that are relative to (or + * suffixes of) the current pattern. + * + * @example + * The following two matchers are equivalent: + *
    + * new UrlMatcher('/user/{id}?q').concat('/details?date');
    + * new UrlMatcher('/user/{id}/details?q&date');
    + * 
    + * + * @param {string} pattern The pattern to append. + * @param {Object} config An object hash of the configuration for the matcher. + * @returns {UrlMatcher} A matcher for the concatenated pattern. + */ +UrlMatcher.prototype.concat = function (pattern, config) { + // Because order of search parameters is irrelevant, we can add our own search + // parameters to the end of the new pattern. Parse the new pattern by itself + // and then join the bits together, but it's much easier to do this on a string level. + var defaultConfig = { + caseInsensitive: $$UMFP.caseInsensitive(), + strict: $$UMFP.strictMode(), + squash: $$UMFP.defaultSquashPolicy() + }; + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); +}; + +UrlMatcher.prototype.toString = function () { + return this.source; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#exec + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Tests the specified path against this matcher, and returns an object containing the captured + * parameter values, or null if the path does not match. The returned object contains the values + * of any search parameters that are mentioned in the pattern, but their value may be null if + * they are not present in `searchParams`. This means that search parameters are always treated + * as optional. + * + * @example + *
    + * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
    + *   x: '1', q: 'hello'
    + * });
    + * // returns { id: 'bob', q: 'hello', r: null }
    + * 
    + * + * @param {string} path The URL path to match, e.g. `$location.path()`. + * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @returns {Object} The captured parameter values. + */ +UrlMatcher.prototype.exec = function (path, searchParams) { + var m = this.regexp.exec(path); + if (!m) return null; + searchParams = searchParams || {}; + + var paramNames = this.parameters(), nTotal = paramNames.length, + nPath = this.segments.length - 1, + values = {}, i, j, cfg, paramName; + + if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + + function decodePathArray(string) { + function reverseString(str) { return str.split("").reverse().join(""); } + function unquoteDashes(str) { return str.replace(/\\-/g, "-"); } + + var split = reverseString(string).split(/-(?!\\)/); + var allReversed = map(split, reverseString); + return map(allReversed, unquoteDashes).reverse(); + } + + for (i = 0; i < nPath; i++) { + paramName = paramNames[i]; + var param = this.params[paramName]; + var paramVal = m[i+1]; + // if the param value matches a pre-replace pair, replace the value before decoding. + for (j = 0; j < param.replace; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); + values[paramName] = param.value(paramVal); + } + for (/**/; i < nTotal; i++) { + paramName = paramNames[i]; + values[paramName] = this.params[paramName].value(searchParams[paramName]); + } + + return values; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#parameters + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns the names of all path and search parameters of this pattern in an unspecified order. + * + * @returns {Array.} An array of parameter names. Must be treated as read-only. If the + * pattern has no parameters, an empty array is returned. + */ +UrlMatcher.prototype.parameters = function (param) { + if (!isDefined(param)) return this.$$paramNames; + return this.params[param] || null; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#validate + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param {Object} params The object hash of parameters to validate. + * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. + */ +UrlMatcher.prototype.validates = function (params) { + return this.params.$$validates(params); +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#format + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Creates a URL that matches this pattern by substituting the specified values + * for the path and search parameters. Null values for path parameters are + * treated as empty strings. + * + * @example + *
    + * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
    + * // returns '/user/bob?q=yes'
    + * 
    + * + * @param {Object} values the values to substitute for the parameters in this pattern. + * @returns {string} the formatted URL (path and optionally search part). + */ +UrlMatcher.prototype.format = function (values) { + values = values || {}; + var segments = this.segments, params = this.parameters(), paramset = this.params; + if (!this.validates(values)) return null; + + var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; + + function encodeDashes(str) { // Replace dashes with encoded "\-" + return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); + } + + for (i = 0; i < nTotal; i++) { + var isPathParam = i < nPath; + var name = params[i], param = paramset[name], value = param.value(values[name]); + var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); + var squash = isDefaultValue ? param.squash : false; + var encoded = param.type.encode(value); + + if (isPathParam) { + var nextSegment = segments[i + 1]; + if (squash === false) { + if (encoded != null) { + if (isArray(encoded)) { + result += map(encoded, encodeDashes).join("-"); + } else { + result += encodeURIComponent(encoded); + } + } + result += nextSegment; + } else if (squash === true) { + var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; + result += nextSegment.match(capture)[1]; + } else if (isString(squash)) { + result += squash + nextSegment; + } + } else { + if (encoded == null || (isDefaultValue && squash !== false)) continue; + if (!isArray(encoded)) encoded = [ encoded ]; + encoded = map(encoded, encodeURIComponent).join('&' + name + '='); + result += (search ? '&' : '?') + (name + '=' + encoded); + search = true; + } + } + + return result; +}; + +/** + * @ngdoc object + * @name ui.router.util.type:Type + * + * @description + * Implements an interface to define custom parameter types that can be decoded from and encoded to + * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} + * objects when matching or formatting URLs, or comparing or validating parameter values. + * + * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more + * information on registering custom types. + * + * @param {Object} config A configuration object which contains the custom type definition. The object's + * properties will override the default methods and/or pattern in `Type`'s public interface. + * @example + *
    + * {
    + *   decode: function(val) { return parseInt(val, 10); },
    + *   encode: function(val) { return val && val.toString(); },
    + *   equals: function(a, b) { return this.is(a) && a === b; },
    + *   is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
    + *   pattern: /\d+/
    + * }
    + * 
    + * + * @property {RegExp} pattern The regular expression pattern used to match values of this type when + * coming from a substring of a URL. + * + * @returns {Object} Returns a new `Type` object. + */ +function Type(config) { + extend(this, config); +} + +/** + * @ngdoc function + * @name ui.router.util.type:Type#is + * @methodOf ui.router.util.type:Type + * + * @description + * Detects whether a value is of a particular type. Accepts a native (decoded) value + * and determines whether it matches the current `Type` object. + * + * @param {*} val The value to check. + * @param {string} key Optional. If the type check is happening in the context of a specific + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the + * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. + * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. + */ +Type.prototype.is = function(val, key) { + return true; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#encode + * @methodOf ui.router.util.type:Type + * + * @description + * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the + * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it + * only needs to be a representation of `val` that has been coerced to a string. + * + * @param {*} val The value to encode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {string} Returns a string representation of `val` that can be encoded in a URL. + */ +Type.prototype.encode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#decode + * @methodOf ui.router.util.type:Type + * + * @description + * Converts a parameter value (from URL string or transition param) to a custom/native value. + * + * @param {string} val The URL parameter value to decode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {*} Returns a custom representation of the URL parameter value. + */ +Type.prototype.decode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#equals + * @methodOf ui.router.util.type:Type + * + * @description + * Determines whether two decoded values are equivalent. + * + * @param {*} a A value to compare against. + * @param {*} b A value to compare against. + * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. + */ +Type.prototype.equals = function(a, b) { + return a == b; +}; + +Type.prototype.$subPattern = function() { + var sub = this.pattern.toString(); + return sub.substr(1, sub.length - 2); +}; + +Type.prototype.pattern = /.*/; + +Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; + +/** Given an encoded string, or a decoded object, returns a decoded object */ +Type.prototype.$normalize = function(val) { + return this.is(val) ? val : this.decode(val); +}; + +/* + * Wraps an existing custom Type as an array of Type, depending on 'mode'. + * e.g.: + * - urlmatcher pattern "/path?{queryParam[]:int}" + * - url: "/path?queryParam=1&queryParam=2 + * - $stateParams.queryParam will be [1, 2] + * if `mode` is "auto", then + * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 + * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] + */ +Type.prototype.$asArray = function(mode, isSearch) { + if (!mode) return this; + if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); + + function ArrayType(type, mode) { + function bindTo(type, callbackName) { + return function() { + return type[callbackName].apply(type, arguments); + }; + } + + // Wrap non-array value as array + function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } + // Unwrap array value for "auto" mode. Return undefined for empty array. + function arrayUnwrap(val) { + switch(val.length) { + case 0: return undefined; + case 1: return mode === "auto" ? val[0] : val; + default: return val; + } + } + function falsey(val) { return !val; } + + // Wraps type (.is/.encode/.decode) functions to operate on each value of an array + function arrayHandler(callback, allTruthyMode) { + return function handleArray(val) { + val = arrayWrap(val); + var result = map(val, callback); + if (allTruthyMode === true) + return filter(result, falsey).length === 0; + return arrayUnwrap(result); + }; + } + + // Wraps type (.equals) functions to operate on each value of an array + function arrayEqualsHandler(callback) { + return function handleArray(val1, val2) { + var left = arrayWrap(val1), right = arrayWrap(val2); + if (left.length !== right.length) return false; + for (var i = 0; i < left.length; i++) { + if (!callback(left[i], right[i])) return false; + } + return true; + }; + } + + this.encode = arrayHandler(bindTo(type, 'encode')); + this.decode = arrayHandler(bindTo(type, 'decode')); + this.is = arrayHandler(bindTo(type, 'is'), true); + this.equals = arrayEqualsHandler(bindTo(type, 'equals')); + this.pattern = type.pattern; + this.$normalize = arrayHandler(bindTo(type, '$normalize')); + this.name = type.name; + this.$arrayMode = mode; + } + + return new ArrayType(this, mode); +}; + + + +/** + * @ngdoc object + * @name ui.router.util.$urlMatcherFactory + * + * @description + * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory + * is also available to providers under the name `$urlMatcherFactoryProvider`. + */ +function $UrlMatcherFactory() { + $$UMFP = this; + + var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; + + function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } + function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } + + var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { + string: { + encode: valToString, + decode: valFromString, + // TODO: in 1.0, make string .is() return false if value is undefined/null by default. + // In 0.2.x, string params are optional by default for backwards compat + is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; }, + pattern: /[^/]*/ + }, + int: { + encode: valToString, + decode: function(val) { return parseInt(val, 10); }, + is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, + pattern: /\d+/ + }, + bool: { + encode: function(val) { return val ? 1 : 0; }, + decode: function(val) { return parseInt(val, 10) !== 0; }, + is: function(val) { return val === true || val === false; }, + pattern: /0|1/ + }, + date: { + encode: function (val) { + if (!this.is(val)) + return undefined; + return [ val.getFullYear(), + ('0' + (val.getMonth() + 1)).slice(-2), + ('0' + val.getDate()).slice(-2) + ].join("-"); + }, + decode: function (val) { + if (this.is(val)) return val; + var match = this.capture.exec(val); + return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + }, + is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, + equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, + capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ + }, + json: { + encode: angular.toJson, + decode: angular.fromJson, + is: angular.isObject, + equals: angular.equals, + pattern: /[^/]*/ + }, + any: { // does not encode/decode + encode: angular.identity, + decode: angular.identity, + equals: angular.equals, + pattern: /.*/ + } + }; + + function getDefaultConfig() { + return { + strict: isStrictMode, + caseInsensitive: isCaseInsensitive + }; + } + + function isInjectable(value) { + return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + $UrlMatcherFactory.$$getDefaultValue = function(config) { + if (!isInjectable(config.value)) return config.value; + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(config.value); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#caseInsensitive + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URL matching should be case sensitive (the default behavior), or not. + * + * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + * @returns {boolean} the current value of caseInsensitive + */ + this.caseInsensitive = function(value) { + if (isDefined(value)) + isCaseInsensitive = value; + return isCaseInsensitive; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#strictMode + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URLs should match trailing slashes, or not (the default behavior). + * + * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. + * @returns {boolean} the current value of strictMode + */ + this.strictMode = function(value) { + if (isDefined(value)) + isStrictMode = value; + return isStrictMode; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Sets the default behavior when generating or matching URLs with default parameter values. + * + * @param {string} value A string that defines the default parameter URL squashing behavior. + * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL + * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the + * parameter is surrounded by slashes, squash (remove) one slash from the URL + * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) + * the parameter value from the URL and replace it with this string. + */ + this.defaultSquashPolicy = function(value) { + if (!isDefined(value)) return defaultSquashPolicy; + if (value !== true && value !== false && !isString(value)) + throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + defaultSquashPolicy = value; + return value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#compile + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. + * + * @param {string} pattern The URL pattern. + * @param {Object} config The config object hash. + * @returns {UrlMatcher} The UrlMatcher. + */ + this.compile = function (pattern, config) { + return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#isMatcher + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Returns true if the specified object is a `UrlMatcher`, or false otherwise. + * + * @param {Object} object The object to perform the type check against. + * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + this.isMatcher = function (o) { + if (!isObject(o)) return false; + var result = true; + + forEach(UrlMatcher.prototype, function(val, name) { + if (isFunction(val)) { + result = result && (isDefined(o[name]) && isFunction(o[name])); + } + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#type + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to + * generate URLs with typed parameters. + * + * @param {string} name The type name. + * @param {Object|Function} definition The type definition. See + * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * @param {Object|Function} definitionFn (optional) A function that is injected before the app + * runtime starts. The result of this function is merged into the existing `definition`. + * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * + * @returns {Object} Returns `$urlMatcherFactoryProvider`. + * + * @example + * This is a simple example of a custom type that encodes and decodes items from an + * array, using the array index as the URL-encoded value: + * + *
    +   * var list = ['John', 'Paul', 'George', 'Ringo'];
    +   *
    +   * $urlMatcherFactoryProvider.type('listItem', {
    +   *   encode: function(item) {
    +   *     // Represent the list item in the URL using its corresponding index
    +   *     return list.indexOf(item);
    +   *   },
    +   *   decode: function(item) {
    +   *     // Look up the list item by index
    +   *     return list[parseInt(item, 10)];
    +   *   },
    +   *   is: function(item) {
    +   *     // Ensure the item is valid by checking to see that it appears
    +   *     // in the list
    +   *     return list.indexOf(item) > -1;
    +   *   }
    +   * });
    +   *
    +   * $stateProvider.state('list', {
    +   *   url: "/list/{item:listItem}",
    +   *   controller: function($scope, $stateParams) {
    +   *     console.log($stateParams.item);
    +   *   }
    +   * });
    +   *
    +   * // ...
    +   *
    +   * // Changes URL to '/list/3', logs "Ringo" to the console
    +   * $state.go('list', { item: "Ringo" });
    +   * 
    + * + * This is a more complex example of a type that relies on dependency injection to + * interact with services, and uses the parameter name from the URL to infer how to + * handle encoding and decoding parameter values: + * + *
    +   * // Defines a custom type that gets a value from a service,
    +   * // where each service gets different types of values from
    +   * // a backend API:
    +   * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
    +   *
    +   *   // Matches up services to URL parameter names
    +   *   var services = {
    +   *     user: Users,
    +   *     post: Posts
    +   *   };
    +   *
    +   *   return {
    +   *     encode: function(object) {
    +   *       // Represent the object in the URL using its unique ID
    +   *       return object.id;
    +   *     },
    +   *     decode: function(value, key) {
    +   *       // Look up the object by ID, using the parameter
    +   *       // name (key) to call the correct service
    +   *       return services[key].findById(value);
    +   *     },
    +   *     is: function(object, key) {
    +   *       // Check that object is a valid dbObject
    +   *       return angular.isObject(object) && object.id && services[key];
    +   *     }
    +   *     equals: function(a, b) {
    +   *       // Check the equality of decoded objects by comparing
    +   *       // their unique IDs
    +   *       return a.id === b.id;
    +   *     }
    +   *   };
    +   * });
    +   *
    +   * // In a config() block, you can then attach URLs with
    +   * // type-annotated parameters:
    +   * $stateProvider.state('users', {
    +   *   url: "/users",
    +   *   // ...
    +   * }).state('users.item', {
    +   *   url: "/{user:dbObject}",
    +   *   controller: function($scope, $stateParams) {
    +   *     // $stateParams.user will now be an object returned from
    +   *     // the Users service
    +   *   },
    +   *   // ...
    +   * });
    +   * 
    + */ + this.type = function (name, definition, definitionFn) { + if (!isDefined(definition)) return $types[name]; + if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); + + $types[name] = new Type(extend({ name: name }, definition)); + if (definitionFn) { + typeQueue.push({ name: name, def: definitionFn }); + if (!enqueue) flushTypeQueue(); + } + return this; + }; + + // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s + function flushTypeQueue() { + while(typeQueue.length) { + var type = typeQueue.shift(); + if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); + angular.extend($types[type.name], injector.invoke(type.def)); + } + } + + // Register default types. Store them in the prototype of $types. + forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); + $types = inherit($types, {}); + + /* No need to document $get, since it returns this */ + this.$get = ['$injector', function ($injector) { + injector = $injector; + enqueue = false; + flushTypeQueue(); + + forEach(defaultTypes, function(type, name) { + if (!$types[name]) $types[name] = new Type(type); + }); + return this; + }]; + + this.Param = function Param(id, type, config, location) { + var self = this; + config = unwrapShorthand(config); + type = getType(config, type, location); + var arrayMode = getArrayMode(); + type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; + if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) + config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" + var isOptional = config.value !== undefined; + var squash = getSquashPolicy(config, isOptional); + var replace = getReplace(config, arrayMode, isOptional, squash); + + function unwrapShorthand(config) { + var keys = isObject(config) ? objectKeys(config) : []; + var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && + indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; + if (isShorthand) config = { value: config }; + config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; + return config; + } + + function getType(config, urlType, location) { + if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); + if (urlType) return urlType; + if (!config.type) return (location === "config" ? $types.any : $types.string); + return config.type instanceof Type ? config.type : new Type(config.type); + } + + // array config: param name (param[]) overrides default settings. explicit config overrides param name. + function getArrayMode() { + var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; + return extend(arrayDefaults, arrayParamNomenclature, config).array; + } + + /** + * returns false, true, or the squash value to indicate the "default parameter url squash policy". + */ + function getSquashPolicy(config, isOptional) { + var squash = config.squash; + if (!isOptional || squash === false) return false; + if (!isDefined(squash) || squash == null) return defaultSquashPolicy; + if (squash === true || isString(squash)) return squash; + throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + } + + function getReplace(config, arrayMode, isOptional, squash) { + var replace, configuredKeys, defaultPolicy = [ + { from: "", to: (isOptional || arrayMode ? undefined : "") }, + { from: null, to: (isOptional || arrayMode ? undefined : "") } + ]; + replace = isArray(config.replace) ? config.replace : []; + if (isString(squash)) + replace.push({ from: squash, to: undefined }); + configuredKeys = map(replace, function(item) { return item.from; } ); + return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + function $$getDefaultValue() { + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + var defaultValue = injector.invoke(config.$$fn); + if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue)) + throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")"); + return defaultValue; + } + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } + function $replace(value) { + var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); + return replacement.length ? replacement[0] : value; + } + value = $replace(value); + return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value); + } + + function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } + + extend(this, { + id: id, + type: type, + location: location, + array: arrayMode, + squash: squash, + replace: replace, + isOptional: isOptional, + value: $value, + dynamic: undefined, + config: config, + toString: toString + }); + }; + + function ParamSet(params) { + extend(this, params || {}); + } + + ParamSet.prototype = { + $$new: function() { + return inherit(this, extend(new ParamSet(), { $$parent: this})); + }, + $$keys: function () { + var keys = [], chain = [], parent = this, + ignore = objectKeys(ParamSet.prototype); + while (parent) { chain.push(parent); parent = parent.$$parent; } + chain.reverse(); + forEach(chain, function(paramset) { + forEach(objectKeys(paramset), function(key) { + if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); + }); + }); + return keys; + }, + $$values: function(paramValues) { + var values = {}, self = this; + forEach(self.$$keys(), function(key) { + values[key] = self[key].value(paramValues && paramValues[key]); + }); + return values; + }, + $$equals: function(paramValues1, paramValues2) { + var equal = true, self = this; + forEach(self.$$keys(), function(key) { + var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; + if (!self[key].type.equals(left, right)) equal = false; + }); + return equal; + }, + $$validates: function $$validate(paramValues) { + var keys = this.$$keys(), i, param, rawVal, normalized, encoded; + for (i = 0; i < keys.length; i++) { + param = this[keys[i]]; + rawVal = paramValues[keys[i]]; + if ((rawVal === undefined || rawVal === null) && param.isOptional) + break; // There was no parameter value, but the param is optional + normalized = param.type.$normalize(rawVal); + if (!param.type.is(normalized)) + return false; // The value was not of the correct Type, and could not be decoded to the correct Type + encoded = param.type.encode(normalized); + if (angular.isString(encoded) && !param.type.pattern.exec(encoded)) + return false; // The value was of the correct type, but when encoded, did not match the Type's regexp + } + return true; + }, + $$parent: undefined + }; + + this.ParamSet = ParamSet; +} + +// Register as a provider so it's available to other providers +angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); +angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); + +/** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider + * + * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider + * + * @description + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * a url in a state configuration. All urls are compiled into a UrlMatcher object. + * + * There are several methods on `$urlRouterProvider` that make it useful to use directly + * in your module config. + */ +$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; +function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { + var rules = [], otherwise = null, interceptDeferred = false, listener; + + // Returns a string that is a prefix of all strings matching the RegExp + function regExpPrefix(re) { + var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); + return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ''; + } + + // Interpolates matched values into a String.replace()-style pattern + function interpolate(pattern, match) { + return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === '$' ? 0 : Number(what)]; + }); + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#rule + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines rules that are used by `$urlRouterProvider` to find matches for + * specific URLs. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   // Here's an example of how you might allow case insensitive urls
    +   *   $urlRouterProvider.rule(function ($injector, $location) {
    +   *     var path = $location.path(),
    +   *         normalized = path.toLowerCase();
    +   *
    +   *     if (path !== normalized) {
    +   *       return normalized;
    +   *     }
    +   *   });
    +   * });
    +   * 
    + * + * @param {object} rule Handler function that takes `$injector` and `$location` + * services as arguments. You can use them to return a valid path as a string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.rule = function (rule) { + if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rules.push(rule); + return this; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider#otherwise + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines a path that is used when an invalid route is requested. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   // if the path doesn't match any of the urls you configured
    +   *   // otherwise will take care of routing the user to the
    +   *   // specified url
    +   *   $urlRouterProvider.otherwise('/index');
    +   *
    +   *   // Example of using function rule as param
    +   *   $urlRouterProvider.otherwise(function ($injector, $location) {
    +   *     return '/a/valid/url';
    +   *   });
    +   * });
    +   * 
    + * + * @param {string|object} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: + * `$injector` and `$location` services, and must return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.otherwise = function (rule) { + if (isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + otherwise = rule; + return this; + }; + + + function handleIfMatch($injector, handler, match) { + if (!match) return false; + var result = $injector.invoke(handler, handler, { $match: match }); + return isDefined(result) ? result : true; + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#when + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Registers a handler for a given url matching. if handle is a string, it is + * treated as a redirect, and is interpolated according to the syntax of match + * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). + * + * If the handler is a function, it is injectable. It gets invoked if `$location` + * matches. You have the option of inject the match object as `$match`. + * + * The handler can return + * + * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` + * will continue trying to find another one that matches. + * - **string** which is treated as a redirect and passed to `$location.url()` + * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   $urlRouterProvider.when($state.url, function ($match, $stateParams) {
    +   *     if ($state.$current.navigable !== state ||
    +   *         !equalForKeys($match, $stateParams) {
    +   *      $state.transitionTo(state, $match, false);
    +   *     }
    +   *   });
    +   * });
    +   * 
    + * + * @param {string|object} what The incoming path that you want to redirect. + * @param {string|object} handler The path you want to redirect your user to. + */ + this.when = function (what, handler) { + var redirect, handlerIsString = isString(handler); + if (isString(what)) what = $urlMatcherFactory.compile(what); + + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) + throw new Error("invalid 'handler' in when()"); + + var strategies = { + matcher: function (what, handler) { + if (handlerIsString) { + redirect = $urlMatcherFactory.compile(handler); + handler = ['$match', function ($match) { return redirect.format($match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); + }, { + prefix: isString(what.prefix) ? what.prefix : '' + }); + }, + regex: function (what, handler) { + if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); + + if (handlerIsString) { + redirect = handler; + handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, { + prefix: regExpPrefix(what) + }); + } + }; + + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + + for (var n in check) { + if (check[n]) return this.rule(strategies[n](what, handler)); + } + + throw new Error("invalid 'what' in when()"); + }; + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#deferIntercept + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Disables (or enables) deferring location change interception. + * + * If you wish to customize the behavior of syncing the URL (for example, if you wish to + * defer a transition but maintain the current URL), call this method at configuration time. + * Then, at run time, call `$urlRouter.listen()` after you have configured your own + * `$locationChangeSuccess` event handler. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *
    +   *   // Prevent $urlRouter from automatically intercepting URL changes;
    +   *   // this allows you to configure custom behavior in between
    +   *   // location changes and route synchronization:
    +   *   $urlRouterProvider.deferIntercept();
    +   *
    +   * }).run(function ($rootScope, $urlRouter, UserService) {
    +   *
    +   *   $rootScope.$on('$locationChangeSuccess', function(e) {
    +   *     // UserService is an example service for managing user state
    +   *     if (UserService.isLoggedIn()) return;
    +   *
    +   *     // Prevent $urlRouter's default handler from firing
    +   *     e.preventDefault();
    +   *
    +   *     UserService.handleLogin().then(function() {
    +   *       // Once the user has logged in, sync the current URL
    +   *       // to the router:
    +   *       $urlRouter.sync();
    +   *     });
    +   *   });
    +   *
    +   *   // Configures $urlRouter's listener *after* your custom listener
    +   *   $urlRouter.listen();
    +   * });
    +   * 
    + * + * @param {boolean} defer Indicates whether to defer location change interception. Passing + no parameter is equivalent to `true`. + */ + this.deferIntercept = function (defer) { + if (defer === undefined) defer = true; + interceptDeferred = defer; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouter + * + * @requires $location + * @requires $rootScope + * @requires $injector + * @requires $browser + * + * @description + * + */ + this.$get = $get; + $get.$inject = ['$location', '$rootScope', '$injector', '$browser']; + function $get( $location, $rootScope, $injector, $browser) { + + var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl; + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === '/') return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; + lastPushedUrl = undefined; + // TODO: Re-implement this in 1.0 for https://github.com/angular-ui/ui-router/issues/1573 + //if (ignoreUpdate) return true; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) return false; + if (isString(handled)) $location.replace().url(handled); + return true; + } + var n = rules.length, i; + + for (i = 0; i < n; i++) { + if (check(rules[i])) return; + } + // always check otherwise last to allow dynamic updates to the set of rules + if (otherwise) check(otherwise); + } + + function listen() { + listener = listener || $rootScope.$on('$locationChangeSuccess', update); + return listener; + } + + if (!interceptDeferred) listen(); + + return { + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#sync + * @methodOf ui.router.router.$urlRouter + * + * @description + * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. + * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, + * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed + * with the transition by calling `$urlRouter.sync()`. + * + * @example + *
    +       * angular.module('app', ['ui.router'])
    +       *   .run(function($rootScope, $urlRouter) {
    +       *     $rootScope.$on('$locationChangeSuccess', function(evt) {
    +       *       // Halt state change from even starting
    +       *       evt.preventDefault();
    +       *       // Perform custom logic
    +       *       var meetsRequirement = ...
    +       *       // Continue with the update and state transition if logic allows
    +       *       if (meetsRequirement) $urlRouter.sync();
    +       *     });
    +       * });
    +       * 
    + */ + sync: function() { + update(); + }, + + listen: function() { + return listen(); + }, + + update: function(read) { + if (read) { + location = $location.url(); + return; + } + if ($location.url() === location) return; + + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + var url = urlMatcher.format(params || {}); + + // Handle the special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + $location.url(url); + lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined; + if (options && options.replace) $location.replace(); + }, + + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#href + * @methodOf ui.router.router.$urlRouter + * + * @description + * A URL generation method that returns the compiled URL for a given + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters. + * + * @example + *
    +       * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
    +       *   person: "bob"
    +       * });
    +       * // $bob == "/about/bob";
    +       * 
    + * + * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate. + * @param {object=} params An object of parameter values to fill the matcher's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + href: function(urlMatcher, params, options) { + if (!urlMatcher.validates(params)) return null; + + var isHtml5 = $locationProvider.html5Mode(); + if (angular.isObject(isHtml5)) { + isHtml5 = isHtml5.enabled; + } + + var url = urlMatcher.format(params); + options = options || {}; + + if (!isHtml5 && url !== null) { + url = "#" + $locationProvider.hashPrefix() + url; + } + + // Handle special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? '/' : ''), port = $location.port(); + port = (port === 80 || port === 443 ? '' : ':' + port); + + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); + } + }; + } +} + +angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); + +/** + * @ngdoc object + * @name ui.router.state.$stateProvider + * + * @requires ui.router.router.$urlRouterProvider + * @requires ui.router.util.$urlMatcherFactoryProvider + * + * @description + * The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely + * on state. + * + * A state corresponds to a "place" in the application in terms of the overall UI and + * navigation. A state describes (via the controller / template / view properties) what + * the UI looks like and does at that place. + * + * States often have things in common, and the primary way of factoring out these + * commonalities in this model is via the state hierarchy, i.e. parent/child states aka + * nested states. + * + * The `$stateProvider` provides interfaces to declare these states for your app. + */ +$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider']; +function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { + + var root, states = {}, $state, queue = {}, abstractKey = 'abstract'; + + // Builds state properties from definition passed to registerState() + var stateBuilder = { + + // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined. + // state.children = []; + // if (parent) parent.children.push(state); + parent: function(state) { + if (isDefined(state.parent) && state.parent) return findState(state.parent); + // regex matches any valid composite state name + // would match "contact.list" but not "contacts" + var compositeName = /^(.+)\.[^.]+$/.exec(state.name); + return compositeName ? findState(compositeName[1]) : root; + }, + + // inherit 'data' from parent and override by own values (if any) + data: function(state) { + if (state.parent && state.parent.data) { + state.data = state.self.data = extend({}, state.parent.data, state.data); + } + return state.data; + }, + + // Build a URLMatcher if necessary, either via a relative or absolute URL + url: function(state) { + var url = state.url, config = { params: state.params || {} }; + + if (isString(url)) { + if (url.charAt(0) == '^') return $urlMatcherFactory.compile(url.substring(1), config); + return (state.parent.navigable || root).url.concat(url, config); + } + + if (!url || $urlMatcherFactory.isMatcher(url)) return url; + throw new Error("Invalid url '" + url + "' in state '" + state + "'"); + }, + + // Keep track of the closest ancestor state that has a URL (i.e. is navigable) + navigable: function(state) { + return state.url ? state : (state.parent ? state.parent.navigable : null); + }, + + // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params + ownParams: function(state) { + var params = state.url && state.url.params || new $$UMFP.ParamSet(); + forEach(state.params || {}, function(config, id) { + if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, "config"); + }); + return params; + }, + + // Derive parameters for this state and ensure they're a super-set of parent's parameters + params: function(state) { + return state.parent && state.parent.params ? extend(state.parent.params.$$new(), state.ownParams) : new $$UMFP.ParamSet(); + }, + + // If there is no explicit multi-view configuration, make one up so we don't have + // to handle both cases in the view directive later. Note that having an explicit + // 'views' property will mean the default unnamed view properties are ignored. This + // is also a good time to resolve view names to absolute names, so everything is a + // straight lookup at link time. + views: function(state) { + var views = {}; + + forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) { + if (name.indexOf('@') < 0) name += '@' + state.parent.name; + views[name] = view; + }); + return views; + }, + + // Keep a full path from the root down to this state as this is needed for state activation. + path: function(state) { + return state.parent ? state.parent.path.concat(state) : []; // exclude root from path + }, + + // Speed up $state.contains() as it's used a lot + includes: function(state) { + var includes = state.parent ? extend({}, state.parent.includes) : {}; + includes[state.name] = true; + return includes; + }, + + $delegates: {} + }; + + function isRelative(stateName) { + return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + } + + function findState(stateOrName, base) { + if (!stateOrName) return undefined; + + var isStr = isString(stateOrName), + name = isStr ? stateOrName : stateOrName.name, + path = isRelative(name); + + if (path) { + if (!base) throw new Error("No reference point given for path '" + name + "'"); + base = findState(base); + + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; + + for (; i < pathLength; i++) { + if (rel[i] === "" && i === 0) { + current = base; + continue; + } + if (rel[i] === "^") { + if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + current = current.parent; + continue; + } + break; + } + rel = rel.slice(i).join("."); + name = current.name + (current.name && rel ? "." : "") + rel; + } + var state = states[name]; + + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { + return state; + } + return undefined; + } + + function queueState(parentName, state) { + if (!queue[parentName]) { + queue[parentName] = []; + } + queue[parentName].push(state); + } + + function flushQueuedChildren(parentName) { + var queued = queue[parentName] || []; + while(queued.length) { + registerState(queued.shift()); + } + } + + function registerState(state) { + // Wrap a new object around the state so we can store our private details easily. + state = inherit(state, { + self: state, + resolve: state.resolve || {}, + toString: function() { return this.name; } + }); + + var name = state.name; + if (!isString(name) || name.indexOf('@') >= 0) throw new Error("State must have a valid name"); + if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined"); + + // Get parent name + var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) + : (isString(state.parent)) ? state.parent + : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name + : ''; + + // If parent is not registered yet, add state to queue and register later + if (parentName && !states[parentName]) { + return queueState(parentName, state.self); + } + + for (var key in stateBuilder) { + if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); + } + states[name] = state; + + // Register the state in the global state list and with $urlRouter if necessary. + if (!state[abstractKey] && state.url) { + $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { + if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { + $state.transitionTo(state, $match, { inherit: true, location: false }); + } + }]); + } + + // Register any queued children + flushQueuedChildren(name); + + return state; + } + + // Checks text to see if it looks like a glob. + function isGlob (text) { + return text.indexOf('*') > -1; + } + + // Returns true if glob matches current $state name. + function doesStateMatchGlob (glob) { + var globSegments = glob.split('.'), + segments = $state.$current.name.split('.'); + + //match single stars + for (var i = 0, l = globSegments.length; i < l; i++) { + if (globSegments[i] === '*') { + segments[i] = '*'; + } + } + + //match greedy starts + if (globSegments[0] === '**') { + segments = segments.slice(indexOf(segments, globSegments[1])); + segments.unshift('**'); + } + //match greedy ends + if (globSegments[globSegments.length - 1] === '**') { + segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.push('**'); + } + + if (globSegments.length != segments.length) { + return false; + } + + return segments.join('') === globSegments.join(''); + } + + + // Implicit root state that is always active + root = registerState({ + name: '', + url: '^', + views: null, + 'abstract': true + }); + root.navigable = null; + + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#decorator + * @methodOf ui.router.state.$stateProvider + * + * @description + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl + * based on the state name. + * + * When passing only a name, it returns the current (original or decorated) builder + * function that matches `name`. + * + * The builder functions that can be decorated are listed below. Though not all + * necessarily have a good use case for decoration, that is up to you to decide. + * + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional + * meta-programming features. + * + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions + * should only be dependent on the state definition object and super function. + * + * + * Existing builder functions and current return values: + * + * - **parent** `{object}` - returns the parent state object. + * - **data** `{object}` - returns state data, including any inherited data that is not + * overridden by own values (if any). + * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} + * or `null`. + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * navigable). + * - **params** `{object}` - returns an array of state params that are ensured to + * be a super-set of parent's params. + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object + * explicitly on a state config, one is still created for you internally. + * So by decorating this builder function you have access to decorating template + * and controller properties. + * - **ownParams** `{object}` - returns an array of params that belong to the state, + * not including any params defined by ancestor states. + * - **path** `{string}` - returns the full path from the root down to this state. + * Needed for state activation. + * - **includes** `{object}` - returns an object that includes every state that + * would pass a `$state.includes()` test. + * + * @example + *
    +   * // Override the internal 'views' builder with a function that takes the state
    +   * // definition, and a reference to the internal function being overridden:
    +   * $stateProvider.decorator('views', function (state, parent) {
    +   *   var result = {},
    +   *       views = parent(state);
    +   *
    +   *   angular.forEach(views, function (config, name) {
    +   *     var autoName = (state.name + '.' + name).replace('.', '/');
    +   *     config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html';
    +   *     result[name] = config;
    +   *   });
    +   *   return result;
    +   * });
    +   *
    +   * $stateProvider.state('home', {
    +   *   views: {
    +   *     'contact.list': { controller: 'ListController' },
    +   *     'contact.item': { controller: 'ItemController' }
    +   *   }
    +   * });
    +   *
    +   * // ...
    +   *
    +   * $state.go('home');
    +   * // Auto-populates list and item views with /partials/home/contact/list.html,
    +   * // and /partials/home/contact/item.html, respectively.
    +   * 
    + * + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original + * builder function. The function receives two parameters: + * + * - `{object}` - state - The state config object. + * - `{object}` - super - The original builder function. + * + * @return {object} $stateProvider - $stateProvider instance + */ + this.decorator = decorator; + function decorator(name, func) { + /*jshint validthis: true */ + if (isString(name) && !isDefined(func)) { + return stateBuilder[name]; + } + if (!isFunction(func) || !isString(name)) { + return this; + } + if (stateBuilder[name] && !stateBuilder.$delegates[name]) { + stateBuilder.$delegates[name] = stateBuilder[name]; + } + stateBuilder[name] = func; + return this; + } + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#state + * @methodOf ui.router.state.$stateProvider + * + * @description + * Registers a state configuration under a given state name. The stateConfig object + * has the following acceptable properties. + * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} stateConfig State configuration object. + * @param {string|function=} stateConfig.template + * + * html template as a string or a function that returns + * an html template as a string which should be used by the uiView directives. This property + * takes precedence over templateUrl. + * + * If `template` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
    template:
    +   *   "

    inline template definition

    " + + * "
    "
    + *
    template: function(params) {
    +   *       return "

    generated template

    "; }
    + * + * + * @param {string|function=} stateConfig.templateUrl + * + * + * path or function that returns a path to an html + * template that should be used by uiView. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
    templateUrl: "home.html"
    + *
    templateUrl: function(params) {
    +   *     return myTemplates[params.pageId]; }
    + * + * @param {function=} stateConfig.templateProvider + * + * Provider function that returns HTML content string. + *
     templateProvider:
    +   *       function(MyTemplateService, params) {
    +   *         return MyTemplateService.getTemplate(params.pageId);
    +   *       }
    + * + * @param {string|function=} stateConfig.controller + * + * + * Controller fn that should be associated with newly + * related scope or the name of a registered controller if passed as a string. + * Optionally, the ControllerAs may be declared here. + *
    controller: "MyRegisteredController"
    + *
    controller:
    +   *     "MyRegisteredController as fooCtrl"}
    + *
    controller: function($scope, MyService) {
    +   *     $scope.data = MyService.getData(); }
    + * + * @param {function=} stateConfig.controllerProvider + * + * + * Injectable provider function that returns the actual controller or string. + *
    controllerProvider:
    +   *   function(MyResolveData) {
    +   *     if (MyResolveData.foo)
    +   *       return "FooCtrl"
    +   *     else if (MyResolveData.bar)
    +   *       return "BarCtrl";
    +   *     else return function($scope) {
    +   *       $scope.baz = "Qux";
    +   *     }
    +   *   }
    + * + * @param {string=} stateConfig.controllerAs + * + * + * A controller alias name. If present the controller will be + * published to scope under the controllerAs name. + *
    controllerAs: "myCtrl"
    + * + * @param {string|object=} stateConfig.parent + * + * Optionally specifies the parent state of this state. + * + *
    parent: 'parentState'
    + *
    parent: parentState // JS variable
    + * + * @param {object=} stateConfig.resolve + * + * + * An optional map<string, function> of dependencies which + * should be injected into the controller. If any of these dependencies are promises, + * the router will wait for them all to be resolved before the controller is instantiated. + * If all the promises are resolved successfully, the $stateChangeSuccess event is fired + * and the values of the resolved promises are injected into any controllers that reference them. + * If any of the promises are rejected the $stateChangeError event is fired. + * + * The map object is: + * + * - key - {string}: name of dependency to be injected into controller + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is + * resolved before its value is injected into controller. + * + *
    resolve: {
    +   *     myResolve1:
    +   *       function($http, $stateParams) {
    +   *         return $http.get("/api/foos/"+stateParams.fooID);
    +   *       }
    +   *     }
    + * + * @param {string=} stateConfig.url + * + * + * A url fragment with optional parameters. When a state is navigated or + * transitioned to, the `$stateParams` service will be populated with any + * parameters that were passed. + * + * (See {@link ui.router.util.type:UrlMatcher UrlMatcher} `UrlMatcher`} for + * more details on acceptable patterns ) + * + * examples: + *
    url: "/home"
    +   * url: "/users/:userid"
    +   * url: "/books/{bookid:[a-zA-Z_-]}"
    +   * url: "/books/{categoryid:int}"
    +   * url: "/books/{publishername:string}/{categoryid:int}"
    +   * url: "/messages?before&after"
    +   * url: "/messages?{before:date}&{after:date}"
    +   * url: "/messages/:mailboxid?{before:date}&{after:date}"
    +   * 
    + * + * @param {object=} stateConfig.views + * + * an optional map<string, object> which defined multiple views, or targets views + * manually/explicitly. + * + * Examples: + * + * Targets three named `ui-view`s in the parent state's template + *
    views: {
    +   *     header: {
    +   *       controller: "headerCtrl",
    +   *       templateUrl: "header.html"
    +   *     }, body: {
    +   *       controller: "bodyCtrl",
    +   *       templateUrl: "body.html"
    +   *     }, footer: {
    +   *       controller: "footCtrl",
    +   *       templateUrl: "footer.html"
    +   *     }
    +   *   }
    + * + * Targets named `ui-view="header"` from grandparent state 'top''s template, and named `ui-view="body" from parent state's template. + *
    views: {
    +   *     'header@top': {
    +   *       controller: "msgHeaderCtrl",
    +   *       templateUrl: "msgHeader.html"
    +   *     }, 'body': {
    +   *       controller: "messagesCtrl",
    +   *       templateUrl: "messages.html"
    +   *     }
    +   *   }
    + * + * @param {boolean=} [stateConfig.abstract=false] + * + * An abstract state will never be directly activated, + * but can provide inherited properties to its common children states. + *
    abstract: true
    + * + * @param {function=} stateConfig.onEnter + * + * + * Callback function for when a state is entered. Good way + * to trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explictly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
    onEnter: function(MyService, $stateParams) {
    +   *     MyService.foo($stateParams.myParam);
    +   * }
    + * + * @param {function=} stateConfig.onExit + * + * + * Callback function for when a state is exited. Good way to + * trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explictly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
    onExit: function(MyService, $stateParams) {
    +   *     MyService.cleanup($stateParams.myParam);
    +   * }
    + * + * @param {boolean=} [stateConfig.reloadOnSearch=true] + * + * + * If `false`, will not retrigger the same state + * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * Useful for when you'd like to modify $location.search() without triggering a reload. + *
    reloadOnSearch: false
    + * + * @param {object=} stateConfig.data + * + * + * Arbitrary data object, useful for custom configuration. The parent state's `data` is + * prototypally inherited. In other words, adding a data property to a state adds it to + * the entire subtree via prototypal inheritance. + * + *
    data: {
    +   *     requiredRole: 'foo'
    +   * } 
    + * + * @param {object=} stateConfig.params + * + * + * A map which optionally configures parameters declared in the `url`, or + * defines additional non-url parameters. For each parameter being + * configured, add a configuration object keyed to the name of the parameter. + * + * Each parameter configuration object may contain the following properties: + * + * - ** value ** - {object|function=}: specifies the default value for this + * parameter. This implicitly sets this parameter as optional. + * + * When UI-Router routes to a state and no value is + * specified for this parameter in the URL or transition, the + * default value will be used instead. If `value` is a function, + * it will be injected and invoked, and the return value used. + * + * *Note*: `undefined` is treated as "no default value" while `null` + * is treated as "the default value is `null`". + * + * *Shorthand*: If you only need to configure the default value of the + * parameter, you may use a shorthand syntax. In the **`params`** + * map, instead mapping the param name to a full parameter configuration + * object, simply set map it to the default parameter value, e.g.: + * + *
    // define a parameter's default value
    +   * params: {
    +   *     param1: { value: "defaultValue" }
    +   * }
    +   * // shorthand default values
    +   * params: {
    +   *     param1: "defaultValue",
    +   *     param2: "param2Default"
    +   * }
    + * + * - ** array ** - {boolean=}: *(default: false)* If true, the param value will be + * treated as an array of values. If you specified a Type, the value will be + * treated as an array of the specified Type. Note: query parameter values + * default to a special `"auto"` mode. + * + * For query parameters in `"auto"` mode, if multiple values for a single parameter + * are present in the URL (e.g.: `/foo?bar=1&bar=2&bar=3`) then the values + * are mapped to an array (e.g.: `{ foo: [ '1', '2', '3' ] }`). However, if + * only one value is present (e.g.: `/foo?bar=1`) then the value is treated as single + * value (e.g.: `{ foo: '1' }`). + * + *
    params: {
    +   *     param1: { array: true }
    +   * }
    + * + * - ** squash ** - {bool|string=}: `squash` configures how a default parameter value is represented in the URL when + * the current parameter value is the same as the default value. If `squash` is not set, it uses the + * configured default squash policy. + * (See {@link ui.router.util.$urlMatcherFactory#methods_defaultSquashPolicy `defaultSquashPolicy()`}) + * + * There are three squash settings: + * + * - false: The parameter's default value is not squashed. It is encoded and included in the URL + * - true: The parameter's default value is omitted from the URL. If the parameter is preceeded and followed + * by slashes in the state's `url` declaration, then one of those slashes are omitted. + * This can allow for cleaner looking URLs. + * - `""`: The parameter's default value is replaced with an arbitrary placeholder of your choice. + * + *
    params: {
    +   *     param1: {
    +   *       value: "defaultId",
    +   *       squash: true
    +   * } }
    +   * // squash "defaultValue" to "~"
    +   * params: {
    +   *     param1: {
    +   *       value: "defaultValue",
    +   *       squash: "~"
    +   * } }
    +   * 
    + * + * + * @example + *
    +   * // Some state name examples
    +   *
    +   * // stateName can be a single top-level name (must be unique).
    +   * $stateProvider.state("home", {});
    +   *
    +   * // Or it can be a nested state name. This state is a child of the
    +   * // above "home" state.
    +   * $stateProvider.state("home.newest", {});
    +   *
    +   * // Nest states as deeply as needed.
    +   * $stateProvider.state("home.newest.abc.xyz.inception", {});
    +   *
    +   * // state() returns $stateProvider, so you can chain state declarations.
    +   * $stateProvider
    +   *   .state("home", {})
    +   *   .state("about", {})
    +   *   .state("contacts", {});
    +   * 
    + * + */ + this.state = state; + function state(name, definition) { + /*jshint validthis: true */ + if (isObject(name)) definition = name; + else definition.name = name; + registerState(definition); + return this; + } + + /** + * @ngdoc object + * @name ui.router.state.$state + * + * @requires $rootScope + * @requires $q + * @requires ui.router.state.$view + * @requires $injector + * @requires ui.router.util.$resolve + * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter + * + * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * you'd like to test against the current active state. + * @property {object} current A reference to the state's config object. However + * you passed it in. Useful for accessing custom data. + * @property {object} transition Currently pending transition. A promise that'll + * resolve or reject. + * + * @description + * `$state` service is responsible for representing states as well as transitioning + * between them. It also provides interfaces to ask for current state or even states + * you're coming from. + */ + this.$get = $get; + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) { + + var TransitionSuperseded = $q.reject(new Error('transition superseded')); + var TransitionPrevented = $q.reject(new Error('transition prevented')); + var TransitionAborted = $q.reject(new Error('transition aborted')); + var TransitionFailed = $q.reject(new Error('transition failed')); + + // Handles the case where a state which is the target of a transition is not found, and the user + // can optionally retry or defer the transition + function handleRedirect(redirect, state, params, options) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateNotFound + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when a requested state **cannot be found** using the provided state name during transition. + * The event is broadcast allowing any handlers a single chance to deal with the error (usually by + * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, + * you can see its three properties in the example. You can use `event.preventDefault()` to abort the + * transition and the promise returned from `go` will be rejected with a `'transition aborted'` value. + * + * @param {Object} event Event object. + * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. + * @param {State} fromState Current state object. + * @param {Object} fromParams Current state params. + * + * @example + * + *
    +       * // somewhere, assume lazy.state has not been defined
    +       * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
    +       *
    +       * // somewhere else
    +       * $scope.$on('$stateNotFound',
    +       * function(event, unfoundState, fromState, fromParams){
    +       *     console.log(unfoundState.to); // "lazy.state"
    +       *     console.log(unfoundState.toParams); // {a:1, b:2}
    +       *     console.log(unfoundState.options); // {inherit:false} + default options
    +       * })
    +       * 
    + */ + var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); + + if (evt.defaultPrevented) { + $urlRouter.update(); + return TransitionAborted; + } + + if (!evt.retry) { + return null; + } + + // Allow the handler to return a promise to defer state lookup retry + if (options.$retry) { + $urlRouter.update(); + return TransitionFailed; + } + var retryTransition = $state.transition = $q.when(evt.retry); + + retryTransition.then(function() { + if (retryTransition !== $state.transition) return TransitionSuperseded; + redirect.options.$retry = true; + return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); + }, function() { + return TransitionAborted; + }); + $urlRouter.update(); + + return retryTransition; + } + + root.locals = { resolve: null, globals: { $stateParams: {} } }; + + $state = { + params: {}, + current: root.self, + $current: root, + transition: null + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#reload + * @methodOf ui.router.state.$state + * + * @description + * A method that force reloads the current state. All resolves are re-resolved, + * controllers reinstantiated, and events re-fired. + * + * @example + *
    +     * var app angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.reload = function(){
    +     *     $state.reload();
    +     *   }
    +     * });
    +     * 
    + * + * `reload()` is just an alias for: + *
    +     * $state.transitionTo($state.current, $stateParams, { 
    +     *   reload: true, inherit: false, notify: true
    +     * });
    +     * 
    + * + * @param {string=|object=} state - A state name or a state object, which is the root of the resolves to be re-resolved. + * @example + *
    +     * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' 
    +     * //and current state is 'contacts.detail.item'
    +     * var app angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.reload = function(){
    +     *     //will reload 'contact.detail' and 'contact.detail.item' states
    +     *     $state.reload('contact.detail');
    +     *   }
    +     * });
    +     * 
    + * + * `reload()` is just an alias for: + *
    +     * $state.transitionTo($state.current, $stateParams, { 
    +     *   reload: true, inherit: false, notify: true
    +     * });
    +     * 
    + + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.reload = function reload(state) { + return $state.transitionTo($state.current, $stateParams, { reload: state || true, inherit: false, notify: true}); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#go + * @methodOf ui.router.state.$state + * + * @description + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters + * inherit from the currently active ancestor states). + * + * @example + *
    +     * var app = angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.changeState = function () {
    +     *     $state.go('contact.detail');
    +     *   };
    +     * });
    +     * 
    + * + * + * @param {string} to Absolute state name or relative state path. Some examples: + * + * - `$state.go('contact.detail')` - will go to the `contact.detail` state + * - `$state.go('^')` - will go to a parent state + * - `$state.go('^.sibling')` - will go to a sibling state + * - `$state.go('.child.grandchild')` - will go to grandchild state + * + * @param {object=} params A map of the parameters that will be sent to the state, + * will populate $stateParams. Any parameters that are not specified will be inherited from currently + * defined parameters. This allows, for example, going to a sibling state that shares parameters + * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. + * transitioning to a sibling will get you the parameters for all parents, transitioning to a child + * will get you all current parameters, etc. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * + * @returns {promise} A promise representing the state of the new transition. + * + * Possible success values: + * + * - $state.current + * + *
    Possible rejection values: + * + * - 'transition superseded' - when a newer transition has been started after this one + * - 'transition prevented' - when `event.preventDefault()` has been called in a `$stateChangeStart` listener + * - 'transition aborted' - when `event.preventDefault()` has been called in a `$stateNotFound` listener or + * when a `$stateNotFound` `event.retry` promise errors. + * - 'transition failed' - when a state has been unsuccessfully found after 2 tries. + * - *resolve error* - when an error has occurred with a `resolve` + * + */ + $state.go = function go(to, params, options) { + return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#transitionTo + * @methodOf ui.router.state.$state + * + * @description + * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} + * uses `transitionTo` internally. `$state.go` is recommended in most situations. + * + * @example + *
    +     * var app = angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.changeState = function () {
    +     *     $state.transitionTo('contact.detail');
    +     *   };
    +     * });
    +     * 
    + * + * @param {string} to State name. + * @param {object=} toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false|string=|object=}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * if String, then will reload the state with the name given in reload, and any children. + * if Object, then a stateObj is expected, will reload the state found in stateObj, and any children. + * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.transitionTo = function transitionTo(to, toParams, options) { + toParams = toParams || {}; + options = extend({ + location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false + }, options || {}); + + var from = $state.$current, fromParams = $state.params, fromPath = from.path; + var evt, toState = findState(to, options.relative); + + // Store the hash param for later (since it will be stripped out by various methods) + var hash = toParams['#']; + + if (!isDefined(toState)) { + var redirect = { to: to, toParams: toParams, options: options }; + var redirectResult = handleRedirect(redirect, from.self, fromParams, options); + + if (redirectResult) { + return redirectResult; + } + + // Always retry once if the $stateNotFound was not prevented + // (handles either redirect changed or state lazy-definition) + to = redirect.to; + toParams = redirect.toParams; + options = redirect.options; + toState = findState(to, options.relative); + + if (!isDefined(toState)) { + if (!options.relative) throw new Error("No such state '" + to + "'"); + throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); + } + } + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + if (!toState.params.$$validates(toParams)) return TransitionFailed; + + toParams = toState.params.$$values(toParams); + to = toState; + + var toPath = to.path; + + // Starting from the root of the path, keep all levels that haven't changed + var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + + if (!options.reload) { + while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } else if (isString(options.reload) || isObject(options.reload)) { + if (isObject(options.reload) && !options.reload.name) { + throw new Error('Invalid reload state object'); + } + + var reloadState = options.reload === true ? fromPath[0] : findState(options.reload); + if (options.reload && !reloadState) { + throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'"); + } + + while (state && state === fromPath[keep] && state !== reloadState) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } + + // If we're going to the same state and all locals are kept, we've got nothing to do. + // But clear 'transition', as we still want to cancel any other pending transitions. + // TODO: We may not want to bump 'transition' if we're called from a location change + // that we've initiated ourselves, because we might accidentally abort a legitimate + // transition initiated from code? + if (shouldSkipReload(to, toParams, from, fromParams, locals, options)) { + if (hash) toParams['#'] = hash; + $state.params = toParams; + copy($state.params, $stateParams); + if (options.location && to.navigable && to.navigable.url) { + $urlRouter.push(to.navigable.url, toParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + $urlRouter.update(true); + } + $state.transition = null; + return $q.when($state.current); + } + + // Filter parameters before we pass them to event handlers etc. + toParams = filterByKeys(to.params.$$keys(), toParams || {}); + + // Broadcast start event and cancel the transition if requested + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeStart + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when the state transition **begins**. You can use `event.preventDefault()` + * to prevent the transition from happening and then the transition promise will be + * rejected with a `'transition prevented'` value. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * + * @example + * + *
    +         * $rootScope.$on('$stateChangeStart',
    +         * function(event, toState, toParams, fromState, fromParams){
    +         *     event.preventDefault();
    +         *     // transitionTo() promise will be rejected with
    +         *     // a 'transition prevented' error
    +         * })
    +         * 
    + */ + if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) { + $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams); + $urlRouter.update(); + return TransitionPrevented; + } + } + + // Resolve locals for the remaining states, but don't update any global state just + // yet -- if anything fails to resolve the current state needs to remain untouched. + // We also set up an inheritance chain for the locals here. This allows the view directive + // to quickly look up the correct definition for each view in the current state. Even + // though we create the locals object itself outside resolveState(), it is initially + // empty and gets filled asynchronously. We need to keep track of the promise for the + // (fully resolved) current locals, and pass this down the chain. + var resolved = $q.when(locals); + + for (var l = keep; l < toPath.length; l++, state = toPath[l]) { + locals = toLocals[l] = inherit(locals); + resolved = resolveState(state, toParams, state === to, resolved, locals, options); + } + + // Once everything is resolved, we are ready to perform the actual transition + // and return a promise for the new state. We also keep track of what the + // current promise is, so that we can detect overlapping transitions and + // keep only the outcome of the last transition. + var transition = $state.transition = resolved.then(function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit 'from' states not kept + for (l = fromPath.length - 1; l >= keep; l--) { + exiting = fromPath[l]; + if (exiting.self.onExit) { + $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + } + exiting.locals = null; + } + + // Enter 'to' states not kept + for (l = keep; l < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } + } + + // Re-add the saved hash before we start returning things + if (hash) toParams['#'] = hash; + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); + } + $urlRouter.update(true); + + return $state.current; + }, function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It's important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); + + if (!evt.defaultPrevented) { + $urlRouter.update(); + } + + return $q.reject(error); + }); + + return transition; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#is + * @methodOf ui.router.state.$state + * + * @description + * Similar to {@link ui.router.state.$state#methods_includes $state.includes}, + * but only checks for the full state name. If params is supplied then it will be + * tested for strict equality against the current active params object, so all params + * must match with none missing and no extras. + * + * @example + *
    +     * $state.$current.name = 'contacts.details.item';
    +     *
    +     * // absolute name
    +     * $state.is('contact.details.item'); // returns true
    +     * $state.is(contactDetailItemStateObject); // returns true
    +     *
    +     * // relative name (. and ^), typically from a template
    +     * // E.g. from the 'contacts.details' template
    +     * 
    Item
    + *
    + * + * @param {string|object} stateOrName The state name (absolute or relative) or state object you'd like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like + * to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object} - If `stateOrName` is a relative state name and `options.relative` is set, .is will + * test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it is the state. + */ + $state.is = function is(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) { return undefined; } + if ($state.$current !== state) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams) : true; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#includes + * @methodOf ui.router.state.$state + * + * @description + * A method to determine if the current active state is equal to or is the child of the + * state stateName. If any params are passed then they will be tested for a match as well. + * Not all the parameters need to be passed, just the ones you'd like to test for equality. + * + * @example + * Partial and relative names + *
    +     * $state.$current.name = 'contacts.details.item';
    +     *
    +     * // Using partial names
    +     * $state.includes("contacts"); // returns true
    +     * $state.includes("contacts.details"); // returns true
    +     * $state.includes("contacts.details.item"); // returns true
    +     * $state.includes("contacts.list"); // returns false
    +     * $state.includes("about"); // returns false
    +     *
    +     * // Using relative names (. and ^), typically from a template
    +     * // E.g. from the 'contacts.details' template
    +     * 
    Item
    + *
    + * + * Basic globbing patterns + *
    +     * $state.$current.name = 'contacts.details.item.url';
    +     *
    +     * $state.includes("*.details.*.*"); // returns true
    +     * $state.includes("*.details.**"); // returns true
    +     * $state.includes("**.item.**"); // returns true
    +     * $state.includes("*.details.item.url"); // returns true
    +     * $state.includes("*.details.*.url"); // returns true
    +     * $state.includes("*.details.*"); // returns false
    +     * $state.includes("item.**"); // returns false
    +     * 
    + * + * @param {string} stateOrName A partial name, relative name, or glob pattern + * to be searched for within the current state name. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, + * that you'd like to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object=} - If `stateOrName` is a relative state reference and `options.relative` is set, + * .includes will test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it does include the state + */ + $state.includes = function includes(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + if (isString(stateOrName) && isGlob(stateOrName)) { + if (!doesStateMatchGlob(stateOrName)) { + return false; + } + stateOrName = $state.$current.name; + } + + var state = findState(stateOrName, options.relative); + if (!isDefined(state)) { return undefined; } + if (!isDefined($state.$current.includes[state.name])) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams, objectKeys(params)) : true; + }; + + + /** + * @ngdoc function + * @name ui.router.state.$state#href + * @methodOf ui.router.state.$state + * + * @description + * A url generation method that returns the compiled url for the given state populated with the given params. + * + * @example + *
    +     * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
    +     * 
    + * + * @param {string|object} stateOrName The state name or state object you'd like to generate a url from. + * @param {object=} params An object of parameter values to fill the state's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the + * first parameter, then the constructed href url will be built from the first navigable ancestor (aka + * ancestor with a valid url). + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} compiled state url + */ + $state.href = function href(stateOrName, params, options) { + options = extend({ + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current + }, options || {}); + + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) return null; + if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = (state && options.lossy) ? state.navigable : state; + + if (!nav || nav.url === undefined || nav.url === null) { + return null; + } + return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys().concat('#'), params || {}), { + absolute: options.absolute + }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#get + * @methodOf ui.router.state.$state + * + * @description + * Returns the state configuration object for any specific state or all states. + * + * @param {string|object=} stateOrName (absolute or relative) If provided, will only get the config for + * the requested state. If not provided, returns an array of ALL state configs. + * @param {string|object=} context When stateOrName is a relative state reference, the state will be retrieved relative to context. + * @returns {Object|Array} State configuration object or array of all objects. + */ + $state.get = function (stateOrName, context) { + if (arguments.length === 0) return map(objectKeys(states), function(name) { return states[name].self; }); + var state = findState(stateOrName, context || $state.$current); + return (state && state.self) ? state.self : null; + }; + + function resolveState(state, params, paramsAreFiltered, inherited, dst, options) { + // Make a restricted $stateParams with only the parameters that apply to this state if + // necessary. In addition to being available to the controller and onEnter/onExit callbacks, + // we also need $stateParams to be available for any $injector calls we make during the + // dependency resolution process. + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params); + var locals = { $stateParams: $stateParams }; + + // Resolve 'global' dependencies for the state, i.e. those not specific to a view. + // We're also including $stateParams in this; that way the parameters are restricted + // to the set that should be visible to the state, and are independent of when we update + // the global $state and $stateParams values. + dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); + var promises = [dst.resolve.then(function (globals) { + dst.globals = globals; + })]; + if (inherited) promises.push(inherited); + + function resolveViews() { + var viewsPromises = []; + + // Resolve template and dependencies for all views. + forEach(state.views, function (view, name) { + var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); + injectables.$template = [ function () { + return $view.load(name, { view: view, locals: dst.globals, params: $stateParams, notify: options.notify }) || ''; + }]; + + viewsPromises.push($resolve.resolve(injectables, dst.globals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, dst.globals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + dst[name] = result; + })); + }); + + return $q.all(viewsPromises).then(function(){ + return dst.globals; + }); + } + + // Wait for all the promises and then return the activation object + return $q.all(promises).then(resolveViews).then(function (values) { + return dst; + }); + } + + return $state; + } + + function shouldSkipReload(to, toParams, from, fromParams, locals, options) { + // Return true if there are no differences in non-search (path/object) params, false if there are differences + function nonSearchParamsEqual(fromAndToState, fromParams, toParams) { + // Identify whether all the parameters that differ between `fromParams` and `toParams` were search params. + function notSearchParam(key) { + return fromAndToState.params[key].location != "search"; + } + var nonQueryParamKeys = fromAndToState.params.$$keys().filter(notSearchParam); + var nonQueryParams = pick.apply({}, [fromAndToState.params].concat(nonQueryParamKeys)); + var nonQueryParamSet = new $$UMFP.ParamSet(nonQueryParams); + return nonQueryParamSet.$$equals(fromParams, toParams); + } + + // If reload was not explicitly requested + // and we're transitioning to the same state we're already in + // and the locals didn't change + // or they changed in a way that doesn't merit reloading + // (reloadOnParams:false, or reloadOnSearch.false and only search params changed) + // Then return true. + if (!options.reload && to === from && + (locals === from.locals || (to.self.reloadOnSearch === false && nonSearchParamsEqual(from, fromParams, toParams)))) { + return true; + } + } +} + +angular.module('ui.router.state') + .value('$stateParams', {}) + .provider('$state', $StateProvider); + + +$ViewProvider.$inject = []; +function $ViewProvider() { + + this.$get = $get; + /** + * @ngdoc object + * @name ui.router.state.$view + * + * @requires ui.router.util.$templateFactory + * @requires $rootScope + * + * @description + * + */ + $get.$inject = ['$rootScope', '$templateFactory']; + function $get( $rootScope, $templateFactory) { + return { + // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) + /** + * @ngdoc function + * @name ui.router.state.$view#load + * @methodOf ui.router.state.$view + * + * @description + * + * @param {string} name name + * @param {object} options option object. + */ + load: function load(name, options) { + var result, defaults = { + template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} + }; + options = extend(defaults, options); + + if (options.view) { + result = $templateFactory.fromConfig(options.view, options.params, options.locals); + } + if (result && options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$viewContentLoading + * @eventOf ui.router.state.$view + * @eventType broadcast on root scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {Object} viewConfig The view config properties (template, controller, etc). + * + * @example + * + *
    +         * $scope.$on('$viewContentLoading',
    +         * function(event, viewConfig){
    +         *     // Access to all the view config properties.
    +         *     // and one special property 'targetView'
    +         *     // viewConfig.targetView
    +         * });
    +         * 
    + */ + $rootScope.$broadcast('$viewContentLoading', options); + } + return result; + } + }; + } +} + +angular.module('ui.router.state').provider('$view', $ViewProvider); + +/** + * @ngdoc object + * @name ui.router.state.$uiViewScrollProvider + * + * @description + * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. + */ +function $ViewScrollProvider() { + + var useAnchorScroll = false; + + /** + * @ngdoc function + * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll + * @methodOf ui.router.state.$uiViewScrollProvider + * + * @description + * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for + * scrolling based on the url anchor. + */ + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScroll + * + * @requires $anchorScroll + * @requires $timeout + * + * @description + * When called with a jqLite element, it scrolls the element into view (after a + * `$timeout` so the DOM has time to refresh). + * + * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, + * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. + */ + this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + return $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; +} + +angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-view + * + * @requires ui.router.state.$state + * @requires $compile + * @requires $controller + * @requires $injector + * @requires ui.router.state.$uiViewScroll + * @requires $document + * + * @restrict ECA + * + * @description + * The ui-view directive tells $state where to place your templates. + * + * @param {string=} name A view name. The name should be unique amongst the other views in the + * same state. You can have views of the same name that live in different states. + * + * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window + * when a view is populated. By default, $anchorScroll is overridden by ui-router's custom scroll + * service, {@link ui.router.state.$uiViewScroll}. This custom service let's you + * scroll ui-view elements into view when they are populated during a state activation. + * + * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) + * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* + * + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @example + * A view can be unnamed or named. + *
    + * 
    + * 
    + * + * + *
    + *
    + * + * You can only have one unnamed view within any template (or root html). If you are only using a + * single view and it is unnamed then you can populate it like so: + *
    + * 
    + * $stateProvider.state("home", { + * template: "

    HELLO!

    " + * }) + *
    + * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} + * config property, by name, in this case an empty name: + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "": {
    + *       template: "

    HELLO!

    " + * } + * } + * }) + *
    + * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, + * but you could if you wanted, like so: + *
    + * 
    + *
    + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "main": {
    + *       template: "

    HELLO!

    " + * } + * } + * }) + *
    + * + * Really though, you'll use views to set up multiple views: + *
    + * 
    + *
    + *
    + *
    + * + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "": {
    + *       template: "

    HELLO!

    " + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + *
    + * + * Examples for `autoscroll`: + * + *
    + * 
    + * 
    + *
    + * 
    + * 
    + * 
    + * 
    + * 
    + */ +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll', '$interpolate']; +function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) { + + function getService() { + return ($injector.has) ? function(service) { + return $injector.has(service) ? $injector.get(service) : null; + } : function(service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; + } + + var service = getService(), + $animator = service('$animator'), + $animate = service('$animate'); + + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { + var statics = function() { + return { + enter: function (element, target, cb) { target.after(element); cb(); }, + leave: function (element, cb) { element.remove(); cb(); } + }; + }; + + if ($animate) { + return { + enter: function(element, target, cb) { + var promise = $animate.enter(element, null, target, cb); + if (promise && promise.then) promise.then(cb); + }, + leave: function(element, cb) { + var promise = $animate.leave(element, cb); + if (promise && promise.then) promise.then(cb); + } + }; + } + + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return { + enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, + leave: function(element, cb) { animate.leave(element); cb(); } + }; + } + + return statics(); + } + + var directive = { + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, latestLocals, + onloadExp = attrs.onload || '', + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope); + + scope.$on('$stateChangeSuccess', function() { + updateView(false); + }); + scope.$on('$viewContentLoading', function() { + updateView(false); + }); + + updateView(true); + + function cleanupLastView() { + if (previousEl) { + previousEl.remove(); + previousEl = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentEl) { + renderer.leave(currentEl, function() { + previousEl = null; + }); + + previousEl = currentEl; + currentEl = null; + } + } + + function updateView(firstTime) { + var newScope, + name = getUiViewName(scope, attrs, $element, $interpolate), + previousLocals = name && $state.$current && $state.$current.locals[name]; + + if (!firstTime && previousLocals === latestLocals) return; // nothing to do + newScope = scope.$new(); + latestLocals = $state.$current.locals[name]; + + var clone = $transclude(newScope, function(clone) { + renderer.enter(clone, $element, function onUiViewEnter() { + if(currentScope) { + currentScope.$emit('$viewContentAnimationEnded'); + } + + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + + currentEl = clone; + currentScope = newScope; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoaded + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description * + * Fired once the view is **loaded**, *after* the DOM is rendered. + * + * @param {Object} event Event object. + */ + currentScope.$emit('$viewContentLoaded'); + currentScope.$eval(onloadExp); + } + }; + } + }; + + return directive; +} + +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state', '$interpolate']; +function $ViewDirectiveFill ( $compile, $controller, $state, $interpolate) { + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + return function (scope, $element, attrs) { + var current = $state.$current, + name = getUiViewName(scope, attrs, $element, $interpolate), + locals = current && current.locals[name]; + + if (! locals) { + return; + } + + $element.data('$uiView', { name: name, state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + locals.$element = $element; + var controller = $controller(locals.$$controller, locals); + if (locals.$$controllerAs) { + scope[locals.$$controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + }; + } + }; +} + +/** + * Shared ui-view code for both directives: + * Given scope, element, and its attributes, return the view's name + */ +function getUiViewName(scope, attrs, element, $interpolate) { + var name = $interpolate(attrs.uiView || attrs.name || '')(scope); + var inherited = element.inheritedData('$uiView'); + return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); +} + +angular.module('ui.router.state').directive('uiView', $ViewDirective); +angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); + +function parseStateRef(ref, current) { + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (preparsed) ref = current + '(' + preparsed[1] + ')'; + parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); + return { state: parsed[1], paramExpr: parsed[3] || null }; +} + +function stateContext(el) { + var stateData = el.parent().inheritedData('$uiView'); + + if (stateData && stateData.state && stateData.state.name) { + return stateData.state; + } +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref + * + * @requires ui.router.state.$state + * @requires $timeout + * + * @restrict A + * + * @description + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. + * + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * handled natively by the browser. + * + * You can also use relative state paths within ui-sref, just like the relative + * paths passed to `$state.go()`. You just need to be aware that the path is relative + * to the state that the link lives in, in other words the state that loaded the + * template containing the link. + * + * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} + * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, + * and `reload`. + * + * @example + * Here's an example of how you'd use ui-sref and how it would compile. If you have the + * following template: + *
    + * Home | About | Next page
    + * 
    + * 
    + * 
    + * + * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): + *
    + * Home | About | Next page
    + * 
    + * 
      + *
    • + * Joe + *
    • + *
    • + * Alice + *
    • + *
    • + * Bob + *
    • + *
    + * + * Home + *
    + * + * @param {string} ui-sref 'stateName' can be any valid absolute or relative state + * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} + */ +$StateRefDirective.$inject = ['$state', '$timeout']; +function $StateRefDirective($state, $timeout) { + var allowedOptions = ['location', 'inherit', 'reload', 'absolute']; + + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var ref = parseStateRef(attrs.uiSref, $state.current.name); + var params = null, url = null, base = stateContext(element) || $state.$current; + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var hrefKind = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? + 'xlink:href' : 'href'; + var newHref = null, isAnchor = element.prop("tagName").toUpperCase() === "A"; + var isForm = element[0].nodeName === "FORM"; + var attr = isForm ? "action" : hrefKind, nav = true; + + var options = { relative: base, inherit: true }; + var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {}; + + angular.forEach(allowedOptions, function(option) { + if (option in optionsOverride) { + options[option] = optionsOverride[option]; + } + }); + + var update = function(newVal) { + if (newVal) params = angular.copy(newVal); + if (!nav) return; + + newHref = $state.href(ref.state, params, options); + + var activeDirective = uiSrefActive[1] || uiSrefActive[0]; + if (activeDirective) { + activeDirective.$$addStateInfo(ref.state, params); + } + if (newHref === null) { + nav = false; + return false; + } + attrs.$set(attr, newHref); + }; + + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function(newVal, oldVal) { + if (newVal !== params) update(newVal); + }, true); + params = angular.copy(scope.$eval(ref.paramExpr)); + } + update(); + + if (isForm) return; + + element.bind("click", function(e) { + var button = e.which || e.button; + if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) { + // HACK: This is to allow ng-clicks to be processed before the transition is initiated: + var transition = $timeout(function() { + $state.go(ref.state, params, options); + }); + e.preventDefault(); + + // if the state has no URL, ignore one preventDefault from the directive. + var ignorePreventDefaultCount = isAnchor && !newHref ? 1: 0; + e.preventDefault = function() { + if (ignorePreventDefaultCount-- <= 0) + $timeout.cancel(transition); + }; + } + }); + } + }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * A directive working alongside ui-sref to add classes to an element when the + * related ui-sref directive's state is active, and removing them when it is inactive. + * The primary use-case is to simplify the special appearance of navigation menus + * relying on `ui-sref`, by having the "active" state's menu button appear different, + * distinguishing it from the inactive menu items. + * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * ui-sref-active found at the same level or above the ui-sref will be used. + * + * Will activate when the ui-sref's target state or any child state is active. If you + * need to activate only when the ui-sref target state is active and *not* any of + * it's children, then you will use + * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} + * + * @example + * Given the following template: + *
    + * 
    + * 
    + * + * + * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the 'active' class): + *
    + * 
    + * 
    + * + * The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * + * Multiple classes may be specified in a space-separated format: + *
    + * 
      + *
    • + * link + *
    • + *
    + *
    + */ + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-eq + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will only activate + * when the exact target state used in the `ui-sref` is active; no child states. + * + */ +$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; +function $StateRefActiveDirective($state, $stateParams, $interpolate) { + return { + restrict: "A", + controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { + var states = [], activeClass; + + // There probably isn't much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope); + + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$addStateInfo = function (newState, newParams) { + var state = $state.get(newState, stateContext($element)); + + states.push({ + state: state || { name: newState }, + params: newParams + }); + + update(); + }; + + $scope.$on('$stateChangeSuccess', update); + + // Update route state + function update() { + if (anyMatch()) { + $element.addClass(activeClass); + } else { + $element.removeClass(activeClass); + } + } + + function anyMatch() { + for (var i = 0; i < states.length; i++) { + if (isMatch(states[i].state, states[i].params)) { + return true; + } + } + return false; + } + + function isMatch(state, params) { + if (typeof $attrs.uiSrefActiveEq !== 'undefined') { + return $state.is(state.name, params); + } else { + return $state.includes(state.name, params); + } + } + }] + }; +} + +angular.module('ui.router.state') + .directive('uiSref', $StateRefDirective) + .directive('uiSrefActive', $StateRefActiveDirective) + .directive('uiSrefActiveEq', $StateRefActiveDirective); + +/** + * @ngdoc filter + * @name ui.router.state.filter:isState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. + */ +$IsStateFilter.$inject = ['$state']; +function $IsStateFilter($state) { + var isFilter = function (state) { + return $state.is(state); + }; + isFilter.$stateful = true; + return isFilter; +} + +/** + * @ngdoc filter + * @name ui.router.state.filter:includedByState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_includes $state.includes('fullOrPartialStateName')}. + */ +$IncludedByStateFilter.$inject = ['$state']; +function $IncludedByStateFilter($state) { + var includesFilter = function (state) { + return $state.includes(state); + }; + includesFilter.$stateful = true; + return includesFilter; +} + +angular.module('ui.router.state') + .filter('isState', $IsStateFilter) + .filter('includedByState', $IncludedByStateFilter); +})(window, window.angular); \ No newline at end of file diff --git a/awx/ui/client/lib/angular-ui-router/release/angular-ui-router.min.js b/awx/ui/client/lib/angular-ui-router/release/angular-ui-router.min.js new file mode 100644 index 0000000000..18d8307f50 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/release/angular-ui-router.min.js @@ -0,0 +1,7 @@ +/** + * State-based routing for AngularJS + * @version v0.2.15 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return N(new(N(function(){},{prototype:a})),b)}function e(a){return M(arguments,function(b){b!==a&&M(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a){if(Object.keys)return Object.keys(a);var b=[];return M(a,function(a,c){b.push(c)}),b}function h(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function i(a,b,c,d){var e,i=f(c,d),j={},k=[];for(var l in i)if(i[l].params&&(e=g(i[l].params),e.length))for(var m in e)h(k,e[m])>=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return N({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(s[c]=d,J(a))q.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);M(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),q.push(c,a,e)}r.pop(),s[c]=f}}function o(a){return K(a)&&a.then&&a.$$promises}if(!K(i))throw new Error("'invocables' must be an object");var p=g(i||{}),q=[],r=[],s={};return M(i,n),i=r=s=null,function(d,f,g){function h(){--u||(v||e(t,f.$$values),r.$$values=t,r.$$promises=r.$$promises||!0,delete r.$$inheritedValues,n.resolve(t))}function i(a){r.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!H(r.$$failure))try{l.resolve(b.invoke(e,g,t)),l.promise.then(function(a){t[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;M(f,function(a){s.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,s[a].then(function(b){t[a]=b,--m||k()},j))}),m||k(),s[c]=l.promise}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!K(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=l;var n=a.defer(),r=n.promise,s=r.$$promises={},t=N({},d),u=1+q.length/3,v=!1;if(H(f.$$failure))return i(f.$$failure),r;f.$$inheritedValues&&e(t,m(f.$$inheritedValues,p)),N(s,f.$$promises),f.$$values?(v=e(t,m(f.$$values,p)),r.$$inheritedValues=m(f.$$values,p),h()):(f.$$inheritedValues&&(r.$$inheritedValues=m(f.$$inheritedValues,p)),f.then(h,i));for(var w=0,x=q.length;x>w;w+=3)d.hasOwnProperty(q[w])?h():j(q[w],q[w+1],q[w+2]);return r}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function q(a,b,c){this.fromConfig=function(a,b,c){return H(a.template)?this.fromString(a.template,b):H(a.templateUrl)?this.fromUrl(a.templateUrl,b):H(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return I(a)?a(b):a},this.fromUrl=function(c,d){return I(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b,headers:{Accept:"text/html"}}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function r(a,b,e){function f(b,c,d,e){if(q.push(b),o[b])return o[b];if(!/^\w+(-+\w+)*(?:\[\])?$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(p[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");return p[b]=new P.Param(b,c,d,e),p[b]}function g(a,b,c,d){var e=["",""],f=a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&");if(!b)return f;switch(c){case!1:e=["(",")"+(d?"?":"")];break;case!0:e=["?(",")?"];break;default:e=["("+c+"|",")?"]}return f+e[0]+b+e[1]}function h(e,f){var g,h,i,j,k;return g=e[2]||e[3],k=b.params[g],i=a.substring(m,e.index),h=f?e[4]:e[4]||("*"==e[1]?".*":null),j=P.type(h||"string")||d(P.type("string"),{pattern:new RegExp(h,b.caseInsensitive?"i":c)}),{id:g,regexp:h,segment:i,type:j,cfg:k}}b=N({params:{}},K(b)?b:{});var i,j=/([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,k=/([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,l="^",m=0,n=this.segments=[],o=e?e.params:{},p=this.params=e?e.params.$$new():new P.ParamSet,q=[];this.source=a;for(var r,s,t;(i=j.exec(a))&&(r=h(i,!1),!(r.segment.indexOf("?")>=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash,s.isOptional),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(b.strict===!1?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function s(a){N(this,a)}function t(){function a(a){return null!=a?a.toString().replace(/\//g,"%2F"):a}function e(a){return null!=a?a.toString().replace(/%2F/g,"/"):a}function f(){return{strict:p,caseInsensitive:m}}function i(a){return I(a)||L(a)&&I(a[a.length-1])}function j(){for(;w.length;){var a=w.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(u[a.name],l.invoke(a.def))}}function k(a){N(this,a||{})}P=this;var l,m=!1,p=!0,q=!1,u={},v=!0,w=[],x={string:{encode:a,decode:e,is:function(a){return null==a||!H(a)||"string"==typeof a},pattern:/[^/]*/},"int":{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return H(a)&&this.decode(a.toString())===a},pattern:/\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return a===!0||a===!1},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^/]*/},any:{encode:b.identity,decode:b.identity,equals:b.equals,pattern:/.*/}};t.$$getDefaultValue=function(a){if(!i(a.value))return a.value;if(!l)throw new Error("Injectable functions cannot be called at configuration time");return l.invoke(a.value)},this.caseInsensitive=function(a){return H(a)&&(m=a),m},this.strictMode=function(a){return H(a)&&(p=a),p},this.defaultSquashPolicy=function(a){if(!H(a))return q;if(a!==!0&&a!==!1&&!J(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return q=a,a},this.compile=function(a,b){return new r(a,N(f(),b))},this.isMatcher=function(a){if(!K(a))return!1;var b=!0;return M(r.prototype,function(c,d){I(c)&&(b=b&&H(a[d])&&I(a[d]))}),b},this.type=function(a,b,c){if(!H(b))return u[a];if(u.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return u[a]=new s(N({name:a},b)),c&&(w.push({name:a,def:c}),v||j()),this},M(x,function(a,b){u[b]=new s(N({name:b},a))}),u=d(u,{}),this.$get=["$injector",function(a){return l=a,v=!1,j(),M(x,function(a,b){u[b]||(u[b]=new s(a))}),this}],this.Param=function(a,b,d,e){function f(a){var b=K(a)?g(a):[],c=-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array");return c&&(a={value:a}),a.$$fn=i(a.value)?a.value:function(){return a.value},a}function j(b,c,d){if(b.type&&c)throw new Error("Param '"+a+"' has two type configurations.");return c?c:b.type?b.type instanceof s?b.type:new s(b.type):"config"===d?u.any:u.string}function k(){var b={array:"search"===e?"auto":!1},c=a.match(/\[\]$/)?{array:!0}:{};return N(b,c,d).array}function m(a,b){var c=a.squash;if(!b||c===!1)return!1;if(!H(c)||null==c)return q;if(c===!0||J(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function p(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=L(a.replace)?a.replace:[],J(e)&&f.push({from:e,to:c}),g=o(f,function(a){return a.from}),n(i,function(a){return-1===h(g,a.from)}).concat(f)}function r(){if(!l)throw new Error("Injectable functions cannot be called at configuration time");var a=l.invoke(d.$$fn);if(null!==a&&a!==c&&!w.type.is(a))throw new Error("Default value ("+a+") for parameter '"+w.id+"' is not an instance of Type ("+w.type.name+")");return a}function t(a){function b(a){return function(b){return b.from===a}}function c(a){var c=o(n(w.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),H(a)?w.type.$normalize(a):r()}function v(){return"{Param:"+a+" "+b+" squash: '"+z+"' optional: "+y+"}"}var w=this;d=f(d),b=j(d,b,e);var x=k();b=x?b.$asArray(x,"search"===e):b,"string"!==b.name||x||"path"!==e||d.value!==c||(d.value="");var y=d.value!==c,z=m(d,y),A=p(d,x,y,z);N(this,{id:a,type:b,location:e,array:x,squash:z,replace:A,isOptional:y,value:t,dynamic:c,config:d,toString:v})},k.prototype={$$new:function(){return d(this,N(new k,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(k.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),M(b,function(b){M(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return M(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return M(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var d,e,f,g,h,i=this.$$keys();for(d=0;de;e++)if(b(j[e]))return;k&&b(k)}}function n(){return i=i||e.$on("$locationChangeSuccess",m)}var o,p=g.baseHref(),q=d.url();return l||n(),{sync:function(){m()},listen:function(){return n()},update:function(a){return a?void(q=d.url()):void(d.url()!==q&&(d.url(q),d.replace()))},push:function(a,b,e){var f=a.format(b||{});null!==f&&b&&b["#"]&&(f+="#"+b["#"]),d.url(f),o=e&&e.$$avoidResync?d.url():c,e&&e.replace&&d.replace()},href:function(c,e,f){if(!c.validates(e))return null;var g=a.html5Mode();b.isObject(g)&&(g=g.enabled);var i=c.format(e);if(f=f||{},g||null===i||(i="#"+a.hashPrefix()+i),null!==i&&e&&e["#"]&&(i+="#"+e["#"]),i=h(i,g,f.absolute),!f.absolute||!i)return i;var j=!g&&i?"/":"",k=d.port();return k=80===k||443===k?"":":"+k,[d.protocol(),"://",d.host(),k,j,i].join("")}}}var i,j=[],k=null,l=!1;this.rule=function(a){if(!I(a))throw new Error("'rule' must be a function");return j.push(a),this},this.otherwise=function(a){if(J(a)){var b=a;a=function(){return b}}else if(!I(a))throw new Error("'rule' must be a function");return k=a,this},this.when=function(a,b){var c,h=J(b);if(J(a)&&(a=d.compile(a)),!h&&!I(b)&&!L(b))throw new Error("invalid 'handler' in when()");var i={matcher:function(a,b){return h&&(c=d.compile(b),b=["$match",function(a){return c.format(a)}]),N(function(c,d){return g(c,b,a.exec(d.path(),d.search()))},{prefix:J(a.prefix)?a.prefix:""})},regex:function(a,b){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(c=b,b=["$match",function(a){return f(c,a)}]),N(function(c,d){return g(c,b,a.exec(d.path()))},{prefix:e(a)})}},j={matcher:d.isMatcher(a),regex:a instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](a,b));throw new Error("invalid 'what' in when()")},this.deferIntercept=function(a){a===c&&(a=!0),l=a},this.$get=h,h.$inject=["$location","$rootScope","$injector","$browser"]}function v(a,e){function f(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function m(a,b){if(!a)return c;var d=J(a),e=d?a:a.name,g=f(e);if(g){if(!b)throw new Error("No reference point given for path '"+e+"'");b=m(b);for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var l=z[e];return!l||!d&&(d||l!==a&&l.self!==a)?c:l}function n(a,b){A[a]||(A[a]=[]),A[a].push(b)}function p(a){for(var b=A[a]||[];b.length;)q(b.shift())}function q(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!J(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(z.hasOwnProperty(c))throw new Error("State '"+c+"'' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):J(b.parent)?b.parent:K(b.parent)&&J(b.parent.name)?b.parent.name:"";if(e&&!z[e])return n(e,b.self);for(var f in C)I(C[f])&&(b[f]=C[f](b,C.$delegates[f]));return z[c]=b,!b[B]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){y.$current.navigable==b&&j(a,c)||y.transitionTo(b,a,{inherit:!0,location:!1})}]),p(c),b}function r(a){return a.indexOf("*")>-1}function s(a){for(var b=a.split("."),c=y.$current.name.split("."),d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return"**"===b[0]&&(c=c.slice(h(c,b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(h(c,b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length?!1:c.join("")===b.join("")}function t(a,b){return J(a)&&!H(b)?C[a]:I(b)&&J(a)?(C[a]&&!C.$delegates[a]&&(C.$delegates[a]=C[a]),C[a]=b,this):this}function u(a,b){return K(a)?b=a:b.name=a,q(b),this}function v(a,e,f,h,l,n,p,q,t){function u(b,c,d,f){var g=a.$broadcast("$stateNotFound",b,c,d);if(g.defaultPrevented)return p.update(),D;if(!g.retry)return null;if(f.$retry)return p.update(),E;var h=y.transition=e.when(g.retry);return h.then(function(){return h!==y.transition?A:(b.options.$retry=!0,y.transitionTo(b.to,b.toParams,b.options))},function(){return D}),p.update(),h}function v(a,c,d,g,i,j){function m(){var c=[];return M(a.views,function(d,e){var g=d.resolve&&d.resolve!==a.resolve?d.resolve:{};g.$template=[function(){return f.load(e,{view:d,locals:i.globals,params:n,notify:j.notify})||""}],c.push(l.resolve(g,i.globals,i.resolve,a).then(function(c){if(I(d.controllerProvider)||L(d.controllerProvider)){var f=b.extend({},g,i.globals);c.$$controller=h.invoke(d.controllerProvider,null,f)}else c.$$controller=d.controller;c.$$state=a,c.$$controllerAs=d.controllerAs,i[e]=c}))}),e.all(c).then(function(){return i.globals})}var n=d?c:k(a.params.$$keys(),c),o={$stateParams:n};i.resolve=l.resolve(a.resolve,o,i.resolve,a);var p=[i.resolve.then(function(a){i.globals=a})];return g&&p.push(g),e.all(p).then(m).then(function(a){return i})}var A=e.reject(new Error("transition superseded")),C=e.reject(new Error("transition prevented")),D=e.reject(new Error("transition aborted")),E=e.reject(new Error("transition failed"));return x.locals={resolve:null,globals:{$stateParams:{}}},y={params:{},current:x.self,$current:x,transition:null},y.reload=function(a){return y.transitionTo(y.current,n,{reload:a||!0,inherit:!1,notify:!0})},y.go=function(a,b,c){return y.transitionTo(a,b,N({inherit:!0,relative:y.$current},c))},y.transitionTo=function(b,c,f){c=c||{},f=N({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,j=y.$current,l=y.params,o=j.path,q=m(b,f.relative),r=c["#"];if(!H(q)){var s={to:b,toParams:c,options:f},t=u(s,j.self,l,f);if(t)return t;if(b=s.to,c=s.toParams,f=s.options,q=m(b,f.relative),!H(q)){if(!f.relative)throw new Error("No such state '"+b+"'");throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'")}}if(q[B])throw new Error("Cannot transition to abstract state '"+b+"'");if(f.inherit&&(c=i(n,c||{},y.$current,q)),!q.params.$$validates(c))return E;c=q.params.$$values(c),b=q;var z=b.path,D=0,F=z[D],G=x.locals,I=[];if(f.reload){if(J(f.reload)||K(f.reload)){if(K(f.reload)&&!f.reload.name)throw new Error("Invalid reload state object");var L=f.reload===!0?o[0]:m(f.reload);if(f.reload&&!L)throw new Error("No such reload state '"+(J(f.reload)?f.reload:f.reload.name)+"'");for(;F&&F===o[D]&&F!==L;)G=I[D]=F.locals,D++,F=z[D]}}else for(;F&&F===o[D]&&F.ownParams.$$equals(c,l);)G=I[D]=F.locals,D++,F=z[D];if(w(b,c,j,l,G,f))return r&&(c["#"]=r),y.params=c,O(y.params,n),f.location&&b.navigable&&b.navigable.url&&(p.push(b.navigable.url,c,{$$avoidResync:!0,replace:"replace"===f.location}),p.update(!0)),y.transition=null,e.when(y.current);if(c=k(b.params.$$keys(),c||{}),f.notify&&a.$broadcast("$stateChangeStart",b.self,c,j.self,l).defaultPrevented)return a.$broadcast("$stateChangeCancel",b.self,c,j.self,l),p.update(),C;for(var M=e.when(G),P=D;P=D;d--)g=o[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=D;d=0?e:e+"@"+(f?f.state.name:"")}function B(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/),!c||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function C(a){var b=a.parent().inheritedData("$uiView");return b&&b.state&&b.state.name?b.state:void 0}function D(a,c){var d=["location","inherit","reload","absolute"];return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(e,f,g,h){var i=B(g.uiSref,a.current.name),j=null,k=C(f)||a.$current,l="[object SVGAnimatedString]"===Object.prototype.toString.call(f.prop("href"))?"xlink:href":"href",m=null,n="A"===f.prop("tagName").toUpperCase(),o="FORM"===f[0].nodeName,p=o?"action":l,q=!0,r={relative:k,inherit:!0},s=e.$eval(g.uiSrefOpts)||{};b.forEach(d,function(a){a in s&&(r[a]=s[a])});var t=function(c){if(c&&(j=b.copy(c)),q){m=a.href(i.state,j,r);var d=h[1]||h[0];return d&&d.$$addStateInfo(i.state,j),null===m?(q=!1,!1):void g.$set(p,m)}};i.paramExpr&&(e.$watch(i.paramExpr,function(a,b){a!==j&&t(a)},!0),j=b.copy(e.$eval(i.paramExpr))),t(),o||f.bind("click",function(b){var d=b.which||b.button;if(!(d>1||b.ctrlKey||b.metaKey||b.shiftKey||f.attr("target"))){var e=c(function(){a.go(i.state,j,r)});b.preventDefault();var g=n&&!m?1:0;b.preventDefault=function(){g--<=0&&c.cancel(e)}}})}}}function E(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs",function(b,d,e){function f(){g()?d.addClass(i):d.removeClass(i)}function g(){for(var a=0;ae;e++){g=h[e];var l=this.params[g],m=d[e+1];for(f=0;fe;e++)g=h[e],k[g]=this.params[g].value(b[g]);return k},r.prototype.parameters=function(a){return H(a)?this.params[a]||null:this.$$paramNames},r.prototype.validates=function(a){return this.params.$$validates(a)},r.prototype.format=function(a){function b(a){return encodeURIComponent(a).replace(/-/g,function(a){return"%5C%"+a.charCodeAt(0).toString(16).toUpperCase()})}a=a||{};var c=this.segments,d=this.parameters(),e=this.params;if(!this.validates(a))return null;var f,g=!1,h=c.length-1,i=d.length,j=c[0];for(f=0;i>f;f++){var k=h>f,l=d[f],m=e[l],n=m.value(a[l]),p=m.isOptional&&m.type.equals(m.value(),n),q=p?m.squash:!1,r=m.type.encode(n);if(k){var s=c[f+1];if(q===!1)null!=r&&(j+=L(r)?o(r,b).join("-"):encodeURIComponent(r)),j+=s;else if(q===!0){var t=j.match(/\/$/)?/\/?(.*)/:/(.*)/;j+=s.match(t)[1]}else J(q)&&(j+=q+s)}else{if(null==r||p&&q!==!1)continue;L(r)||(r=[r]),r=o(r,encodeURIComponent).join("&"+l+"="),j+=(g?"&":"?")+(l+"="+r),g=!0}}return j},s.prototype.is=function(a,b){return!0},s.prototype.encode=function(a,b){return a},s.prototype.decode=function(a,b){return a},s.prototype.equals=function(a,b){return a==b},s.prototype.$subPattern=function(){var a=this.pattern.toString();return a.substr(1,a.length-2)},s.prototype.pattern=/.*/,s.prototype.toString=function(){return"{Type:"+this.name+"}"},s.prototype.$normalize=function(a){return this.is(a)?a:this.decode(a)},s.prototype.$asArray=function(a,b){function d(a,b){function d(a,b){return function(){return a[b].apply(a,arguments)}}function e(a){return L(a)?a:H(a)?[a]:[]}function f(a){switch(a.length){case 0:return c;case 1:return"auto"===b?a[0]:a;default:return a}}function g(a){return!a}function h(a,b){return function(c){c=e(c);var d=o(c,a);return b===!0?0===n(d,g).length:f(d)}}function i(a){return function(b,c){var d=e(b),f=e(c);if(d.length!==f.length)return!1;for(var g=0;g>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; +} + +/** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @param {Object} currentParams The value of the current state parameters ($stateParams). + * @param {Object} newParams The set of parameters which will be composited with inherited params. + * @param {Object} $current Internal definition of object representing the current state. + * @param {Object} $to Internal definition of object representing state to transition to. + */ +function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i].params) continue; + parentParams = objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (indexOf(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); +} + +/** + * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. + * + * @param {Object} a The first object. + * @param {Object} b The second object. + * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, + * it defaults to the list of keys in `a`. + * @return {Boolean} Returns `true` if the keys match, otherwise `false`. + */ +function equalForKeys(a, b, keys) { + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i=0; i + * + * + * + * + * + * + * + * + * + * + * + * + */ +angular.module('ui.router', ['ui.router.state']); + +angular.module('ui.router.compat', ['ui.router']); diff --git a/awx/ui/client/lib/angular-ui-router/src/resolve.js b/awx/ui/client/lib/angular-ui-router/src/resolve.js new file mode 100644 index 0000000000..f1c1790099 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/resolve.js @@ -0,0 +1,252 @@ +/** + * @ngdoc object + * @name ui.router.util.$resolve + * + * @requires $q + * @requires $injector + * + * @description + * Manages resolution of (acyclic) graphs of promises. + */ +$Resolve.$inject = ['$q', '$injector']; +function $Resolve( $q, $injector) { + + var VISIT_IN_PROGRESS = 1, + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + + + /** + * @ngdoc function + * @name ui.router.util.$resolve#study + * @methodOf ui.router.util.$resolve + * + * @description + * Studies a set of invocables that are likely to be used multiple times. + *
    +   * $resolve.study(invocables)(locals, parent, self)
    +   * 
    + * is equivalent to + *
    +   * $resolve.resolve(invocables, locals, parent, self)
    +   * 
    + * but the former is more efficient (in fact `resolve` just calls `study` + * internally). + * + * @param {object} invocables Invocable objects + * @return {function} a function to pass in locals, parent and self + */ + this.study = function (invocables) { + if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + var invocableKeys = objectKeys(invocables || {}); + + // Perform a topological sort of invocables to build an ordered plan + var plan = [], cycle = [], visited = {}; + function visit(value, key) { + if (visited[key] === VISIT_DONE) return; + + cycle.push(key); + if (visited[key] === VISIT_IN_PROGRESS) { + cycle.splice(0, indexOf(cycle, key)); + throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + } + visited[key] = VISIT_IN_PROGRESS; + + if (isString(value)) { + plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); + } else { + var params = $injector.annotate(value); + forEach(params, function (param) { + if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); + }); + plan.push(key, value, params); + } + + cycle.pop(); + visited[key] = VISIT_DONE; + } + forEach(invocables, visit); + invocables = cycle = visited = null; // plan is all that's required + + function isResolve(value) { + return isObject(value) && value.then && value.$$promises; + } + + return function (locals, parent, self) { + if (isResolve(locals) && self === undefined) { + self = parent; parent = locals; locals = null; + } + if (!locals) locals = NO_LOCALS; + else if (!isObject(locals)) { + throw new Error("'locals' must be an object"); + } + if (!parent) parent = NO_PARENT; + else if (!isResolve(parent)) { + throw new Error("'parent' must be a promise returned by $resolve.resolve()"); + } + + // To complete the overall resolution, we have to wait for the parent + // promise and for the promise for each invokable in our plan. + var resolution = $q.defer(), + result = resolution.promise, + promises = result.$$promises = {}, + values = extend({}, locals), + wait = 1 + plan.length/3, + merged = false; + + function done() { + // Merge parent values we haven't got yet and publish our own $$values + if (!--wait) { + if (!merged) merge(values, parent.$$values); + result.$$values = values; + result.$$promises = result.$$promises || true; // keep for isResolve() + delete result.$$inheritedValues; + resolution.resolve(values); + } + } + + function fail(reason) { + result.$$failure = reason; + resolution.reject(reason); + } + + // Short-circuit if parent has already failed + if (isDefined(parent.$$failure)) { + fail(parent.$$failure); + return result; + } + + if (parent.$$inheritedValues) { + merge(values, omit(parent.$$inheritedValues, invocableKeys)); + } + + // Merge parent values if the parent has already resolved, or merge + // parent promises and wait if the parent resolve is still in progress. + extend(promises, parent.$$promises); + if (parent.$$values) { + merged = merge(values, omit(parent.$$values, invocableKeys)); + result.$$inheritedValues = omit(parent.$$values, invocableKeys); + done(); + } else { + if (parent.$$inheritedValues) { + result.$$inheritedValues = omit(parent.$$inheritedValues, invocableKeys); + } + parent.then(done, fail); + } + + // Process each invocable in the plan, but ignore any where a local of the same name exists. + for (var i=0, ii=plan.length; i= 0) throw new Error("State must have a valid name"); + if (states.hasOwnProperty(name)) throw new Error("State '" + name + "'' is already defined"); + + // Get parent name + var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.')) + : (isString(state.parent)) ? state.parent + : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name + : ''; + + // If parent is not registered yet, add state to queue and register later + if (parentName && !states[parentName]) { + return queueState(parentName, state.self); + } + + for (var key in stateBuilder) { + if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); + } + states[name] = state; + + // Register the state in the global state list and with $urlRouter if necessary. + if (!state[abstractKey] && state.url) { + $urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) { + if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { + $state.transitionTo(state, $match, { inherit: true, location: false }); + } + }]); + } + + // Register any queued children + flushQueuedChildren(name); + + return state; + } + + // Checks text to see if it looks like a glob. + function isGlob (text) { + return text.indexOf('*') > -1; + } + + // Returns true if glob matches current $state name. + function doesStateMatchGlob (glob) { + var globSegments = glob.split('.'), + segments = $state.$current.name.split('.'); + + //match single stars + for (var i = 0, l = globSegments.length; i < l; i++) { + if (globSegments[i] === '*') { + segments[i] = '*'; + } + } + + //match greedy starts + if (globSegments[0] === '**') { + segments = segments.slice(indexOf(segments, globSegments[1])); + segments.unshift('**'); + } + //match greedy ends + if (globSegments[globSegments.length - 1] === '**') { + segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.push('**'); + } + + if (globSegments.length != segments.length) { + return false; + } + + return segments.join('') === globSegments.join(''); + } + + + // Implicit root state that is always active + root = registerState({ + name: '', + url: '^', + views: null, + 'abstract': true + }); + root.navigable = null; + + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#decorator + * @methodOf ui.router.state.$stateProvider + * + * @description + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl + * based on the state name. + * + * When passing only a name, it returns the current (original or decorated) builder + * function that matches `name`. + * + * The builder functions that can be decorated are listed below. Though not all + * necessarily have a good use case for decoration, that is up to you to decide. + * + * In addition, users can attach custom decorators, which will generate new + * properties within the state's internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional + * meta-programming features. + * + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions + * should only be dependent on the state definition object and super function. + * + * + * Existing builder functions and current return values: + * + * - **parent** `{object}` - returns the parent state object. + * - **data** `{object}` - returns state data, including any inherited data that is not + * overridden by own values (if any). + * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} + * or `null`. + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * navigable). + * - **params** `{object}` - returns an array of state params that are ensured to + * be a super-set of parent's params. + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don't use the views object + * explicitly on a state config, one is still created for you internally. + * So by decorating this builder function you have access to decorating template + * and controller properties. + * - **ownParams** `{object}` - returns an array of params that belong to the state, + * not including any params defined by ancestor states. + * - **path** `{string}` - returns the full path from the root down to this state. + * Needed for state activation. + * - **includes** `{object}` - returns an object that includes every state that + * would pass a `$state.includes()` test. + * + * @example + *
    +   * // Override the internal 'views' builder with a function that takes the state
    +   * // definition, and a reference to the internal function being overridden:
    +   * $stateProvider.decorator('views', function (state, parent) {
    +   *   var result = {},
    +   *       views = parent(state);
    +   *
    +   *   angular.forEach(views, function (config, name) {
    +   *     var autoName = (state.name + '.' + name).replace('.', '/');
    +   *     config.templateUrl = config.templateUrl || '/partials/' + autoName + '.html';
    +   *     result[name] = config;
    +   *   });
    +   *   return result;
    +   * });
    +   *
    +   * $stateProvider.state('home', {
    +   *   views: {
    +   *     'contact.list': { controller: 'ListController' },
    +   *     'contact.item': { controller: 'ItemController' }
    +   *   }
    +   * });
    +   *
    +   * // ...
    +   *
    +   * $state.go('home');
    +   * // Auto-populates list and item views with /partials/home/contact/list.html,
    +   * // and /partials/home/contact/item.html, respectively.
    +   * 
    + * + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original + * builder function. The function receives two parameters: + * + * - `{object}` - state - The state config object. + * - `{object}` - super - The original builder function. + * + * @return {object} $stateProvider - $stateProvider instance + */ + this.decorator = decorator; + function decorator(name, func) { + /*jshint validthis: true */ + if (isString(name) && !isDefined(func)) { + return stateBuilder[name]; + } + if (!isFunction(func) || !isString(name)) { + return this; + } + if (stateBuilder[name] && !stateBuilder.$delegates[name]) { + stateBuilder.$delegates[name] = stateBuilder[name]; + } + stateBuilder[name] = func; + return this; + } + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#state + * @methodOf ui.router.state.$stateProvider + * + * @description + * Registers a state configuration under a given state name. The stateConfig object + * has the following acceptable properties. + * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} stateConfig State configuration object. + * @param {string|function=} stateConfig.template + * + * html template as a string or a function that returns + * an html template as a string which should be used by the uiView directives. This property + * takes precedence over templateUrl. + * + * If `template` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
    template:
    +   *   "

    inline template definition

    " + + * "
    "
    + *
    template: function(params) {
    +   *       return "

    generated template

    "; }
    + * + * + * @param {string|function=} stateConfig.templateUrl + * + * + * path or function that returns a path to an html + * template that should be used by uiView. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + *
    templateUrl: "home.html"
    + *
    templateUrl: function(params) {
    +   *     return myTemplates[params.pageId]; }
    + * + * @param {function=} stateConfig.templateProvider + * + * Provider function that returns HTML content string. + *
     templateProvider:
    +   *       function(MyTemplateService, params) {
    +   *         return MyTemplateService.getTemplate(params.pageId);
    +   *       }
    + * + * @param {string|function=} stateConfig.controller + * + * + * Controller fn that should be associated with newly + * related scope or the name of a registered controller if passed as a string. + * Optionally, the ControllerAs may be declared here. + *
    controller: "MyRegisteredController"
    + *
    controller:
    +   *     "MyRegisteredController as fooCtrl"}
    + *
    controller: function($scope, MyService) {
    +   *     $scope.data = MyService.getData(); }
    + * + * @param {function=} stateConfig.controllerProvider + * + * + * Injectable provider function that returns the actual controller or string. + *
    controllerProvider:
    +   *   function(MyResolveData) {
    +   *     if (MyResolveData.foo)
    +   *       return "FooCtrl"
    +   *     else if (MyResolveData.bar)
    +   *       return "BarCtrl";
    +   *     else return function($scope) {
    +   *       $scope.baz = "Qux";
    +   *     }
    +   *   }
    + * + * @param {string=} stateConfig.controllerAs + * + * + * A controller alias name. If present the controller will be + * published to scope under the controllerAs name. + *
    controllerAs: "myCtrl"
    + * + * @param {string|object=} stateConfig.parent + * + * Optionally specifies the parent state of this state. + * + *
    parent: 'parentState'
    + *
    parent: parentState // JS variable
    + * + * @param {object=} stateConfig.resolve + * + * + * An optional map<string, function> of dependencies which + * should be injected into the controller. If any of these dependencies are promises, + * the router will wait for them all to be resolved before the controller is instantiated. + * If all the promises are resolved successfully, the $stateChangeSuccess event is fired + * and the values of the resolved promises are injected into any controllers that reference them. + * If any of the promises are rejected the $stateChangeError event is fired. + * + * The map object is: + * + * - key - {string}: name of dependency to be injected into controller + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is + * resolved before its value is injected into controller. + * + *
    resolve: {
    +   *     myResolve1:
    +   *       function($http, $stateParams) {
    +   *         return $http.get("/api/foos/"+stateParams.fooID);
    +   *       }
    +   *     }
    + * + * @param {string=} stateConfig.url + * + * + * A url fragment with optional parameters. When a state is navigated or + * transitioned to, the `$stateParams` service will be populated with any + * parameters that were passed. + * + * (See {@link ui.router.util.type:UrlMatcher UrlMatcher} `UrlMatcher`} for + * more details on acceptable patterns ) + * + * examples: + *
    url: "/home"
    +   * url: "/users/:userid"
    +   * url: "/books/{bookid:[a-zA-Z_-]}"
    +   * url: "/books/{categoryid:int}"
    +   * url: "/books/{publishername:string}/{categoryid:int}"
    +   * url: "/messages?before&after"
    +   * url: "/messages?{before:date}&{after:date}"
    +   * url: "/messages/:mailboxid?{before:date}&{after:date}"
    +   * 
    + * + * @param {object=} stateConfig.views + * + * an optional map<string, object> which defined multiple views, or targets views + * manually/explicitly. + * + * Examples: + * + * Targets three named `ui-view`s in the parent state's template + *
    views: {
    +   *     header: {
    +   *       controller: "headerCtrl",
    +   *       templateUrl: "header.html"
    +   *     }, body: {
    +   *       controller: "bodyCtrl",
    +   *       templateUrl: "body.html"
    +   *     }, footer: {
    +   *       controller: "footCtrl",
    +   *       templateUrl: "footer.html"
    +   *     }
    +   *   }
    + * + * Targets named `ui-view="header"` from grandparent state 'top''s template, and named `ui-view="body" from parent state's template. + *
    views: {
    +   *     'header@top': {
    +   *       controller: "msgHeaderCtrl",
    +   *       templateUrl: "msgHeader.html"
    +   *     }, 'body': {
    +   *       controller: "messagesCtrl",
    +   *       templateUrl: "messages.html"
    +   *     }
    +   *   }
    + * + * @param {boolean=} [stateConfig.abstract=false] + * + * An abstract state will never be directly activated, + * but can provide inherited properties to its common children states. + *
    abstract: true
    + * + * @param {function=} stateConfig.onEnter + * + * + * Callback function for when a state is entered. Good way + * to trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explictly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
    onEnter: function(MyService, $stateParams) {
    +   *     MyService.foo($stateParams.myParam);
    +   * }
    + * + * @param {function=} stateConfig.onExit + * + * + * Callback function for when a state is exited. Good way to + * trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to explictly annotate this function, + * because it won't be automatically annotated by your build tools. + * + *
    onExit: function(MyService, $stateParams) {
    +   *     MyService.cleanup($stateParams.myParam);
    +   * }
    + * + * @param {boolean=} [stateConfig.reloadOnSearch=true] + * + * + * If `false`, will not retrigger the same state + * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * Useful for when you'd like to modify $location.search() without triggering a reload. + *
    reloadOnSearch: false
    + * + * @param {object=} stateConfig.data + * + * + * Arbitrary data object, useful for custom configuration. The parent state's `data` is + * prototypally inherited. In other words, adding a data property to a state adds it to + * the entire subtree via prototypal inheritance. + * + *
    data: {
    +   *     requiredRole: 'foo'
    +   * } 
    + * + * @param {object=} stateConfig.params + * + * + * A map which optionally configures parameters declared in the `url`, or + * defines additional non-url parameters. For each parameter being + * configured, add a configuration object keyed to the name of the parameter. + * + * Each parameter configuration object may contain the following properties: + * + * - ** value ** - {object|function=}: specifies the default value for this + * parameter. This implicitly sets this parameter as optional. + * + * When UI-Router routes to a state and no value is + * specified for this parameter in the URL or transition, the + * default value will be used instead. If `value` is a function, + * it will be injected and invoked, and the return value used. + * + * *Note*: `undefined` is treated as "no default value" while `null` + * is treated as "the default value is `null`". + * + * *Shorthand*: If you only need to configure the default value of the + * parameter, you may use a shorthand syntax. In the **`params`** + * map, instead mapping the param name to a full parameter configuration + * object, simply set map it to the default parameter value, e.g.: + * + *
    // define a parameter's default value
    +   * params: {
    +   *     param1: { value: "defaultValue" }
    +   * }
    +   * // shorthand default values
    +   * params: {
    +   *     param1: "defaultValue",
    +   *     param2: "param2Default"
    +   * }
    + * + * - ** array ** - {boolean=}: *(default: false)* If true, the param value will be + * treated as an array of values. If you specified a Type, the value will be + * treated as an array of the specified Type. Note: query parameter values + * default to a special `"auto"` mode. + * + * For query parameters in `"auto"` mode, if multiple values for a single parameter + * are present in the URL (e.g.: `/foo?bar=1&bar=2&bar=3`) then the values + * are mapped to an array (e.g.: `{ foo: [ '1', '2', '3' ] }`). However, if + * only one value is present (e.g.: `/foo?bar=1`) then the value is treated as single + * value (e.g.: `{ foo: '1' }`). + * + *
    params: {
    +   *     param1: { array: true }
    +   * }
    + * + * - ** squash ** - {bool|string=}: `squash` configures how a default parameter value is represented in the URL when + * the current parameter value is the same as the default value. If `squash` is not set, it uses the + * configured default squash policy. + * (See {@link ui.router.util.$urlMatcherFactory#methods_defaultSquashPolicy `defaultSquashPolicy()`}) + * + * There are three squash settings: + * + * - false: The parameter's default value is not squashed. It is encoded and included in the URL + * - true: The parameter's default value is omitted from the URL. If the parameter is preceeded and followed + * by slashes in the state's `url` declaration, then one of those slashes are omitted. + * This can allow for cleaner looking URLs. + * - `""`: The parameter's default value is replaced with an arbitrary placeholder of your choice. + * + *
    params: {
    +   *     param1: {
    +   *       value: "defaultId",
    +   *       squash: true
    +   * } }
    +   * // squash "defaultValue" to "~"
    +   * params: {
    +   *     param1: {
    +   *       value: "defaultValue",
    +   *       squash: "~"
    +   * } }
    +   * 
    + * + * + * @example + *
    +   * // Some state name examples
    +   *
    +   * // stateName can be a single top-level name (must be unique).
    +   * $stateProvider.state("home", {});
    +   *
    +   * // Or it can be a nested state name. This state is a child of the
    +   * // above "home" state.
    +   * $stateProvider.state("home.newest", {});
    +   *
    +   * // Nest states as deeply as needed.
    +   * $stateProvider.state("home.newest.abc.xyz.inception", {});
    +   *
    +   * // state() returns $stateProvider, so you can chain state declarations.
    +   * $stateProvider
    +   *   .state("home", {})
    +   *   .state("about", {})
    +   *   .state("contacts", {});
    +   * 
    + * + */ + this.state = state; + function state(name, definition) { + /*jshint validthis: true */ + if (isObject(name)) definition = name; + else definition.name = name; + registerState(definition); + return this; + } + + /** + * @ngdoc object + * @name ui.router.state.$state + * + * @requires $rootScope + * @requires $q + * @requires ui.router.state.$view + * @requires $injector + * @requires ui.router.util.$resolve + * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter + * + * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * you'd like to test against the current active state. + * @property {object} current A reference to the state's config object. However + * you passed it in. Useful for accessing custom data. + * @property {object} transition Currently pending transition. A promise that'll + * resolve or reject. + * + * @description + * `$state` service is responsible for representing states as well as transitioning + * between them. It also provides interfaces to ask for current state or even states + * you're coming from. + */ + this.$get = $get; + $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory']; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) { + + var TransitionSuperseded = $q.reject(new Error('transition superseded')); + var TransitionPrevented = $q.reject(new Error('transition prevented')); + var TransitionAborted = $q.reject(new Error('transition aborted')); + var TransitionFailed = $q.reject(new Error('transition failed')); + + // Handles the case where a state which is the target of a transition is not found, and the user + // can optionally retry or defer the transition + function handleRedirect(redirect, state, params, options) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateNotFound + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when a requested state **cannot be found** using the provided state name during transition. + * The event is broadcast allowing any handlers a single chance to deal with the error (usually by + * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, + * you can see its three properties in the example. You can use `event.preventDefault()` to abort the + * transition and the promise returned from `go` will be rejected with a `'transition aborted'` value. + * + * @param {Object} event Event object. + * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. + * @param {State} fromState Current state object. + * @param {Object} fromParams Current state params. + * + * @example + * + *
    +       * // somewhere, assume lazy.state has not been defined
    +       * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
    +       *
    +       * // somewhere else
    +       * $scope.$on('$stateNotFound',
    +       * function(event, unfoundState, fromState, fromParams){
    +       *     console.log(unfoundState.to); // "lazy.state"
    +       *     console.log(unfoundState.toParams); // {a:1, b:2}
    +       *     console.log(unfoundState.options); // {inherit:false} + default options
    +       * })
    +       * 
    + */ + var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params); + + if (evt.defaultPrevented) { + $urlRouter.update(); + return TransitionAborted; + } + + if (!evt.retry) { + return null; + } + + // Allow the handler to return a promise to defer state lookup retry + if (options.$retry) { + $urlRouter.update(); + return TransitionFailed; + } + var retryTransition = $state.transition = $q.when(evt.retry); + + retryTransition.then(function() { + if (retryTransition !== $state.transition) return TransitionSuperseded; + redirect.options.$retry = true; + return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); + }, function() { + return TransitionAborted; + }); + $urlRouter.update(); + + return retryTransition; + } + + root.locals = { resolve: null, globals: { $stateParams: {} } }; + + $state = { + params: {}, + current: root.self, + $current: root, + transition: null + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#reload + * @methodOf ui.router.state.$state + * + * @description + * A method that force reloads the current state. All resolves are re-resolved, + * controllers reinstantiated, and events re-fired. + * + * @example + *
    +     * var app angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.reload = function(){
    +     *     $state.reload();
    +     *   }
    +     * });
    +     * 
    + * + * `reload()` is just an alias for: + *
    +     * $state.transitionTo($state.current, $stateParams, { 
    +     *   reload: true, inherit: false, notify: true
    +     * });
    +     * 
    + * + * @param {string=|object=} state - A state name or a state object, which is the root of the resolves to be re-resolved. + * @example + *
    +     * //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item' 
    +     * //and current state is 'contacts.detail.item'
    +     * var app angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.reload = function(){
    +     *     //will reload 'contact.detail' and 'contact.detail.item' states
    +     *     $state.reload('contact.detail');
    +     *   }
    +     * });
    +     * 
    + * + * `reload()` is just an alias for: + *
    +     * $state.transitionTo($state.current, $stateParams, { 
    +     *   reload: true, inherit: false, notify: true
    +     * });
    +     * 
    + + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.reload = function reload(state) { + return $state.transitionTo($state.current, $stateParams, { reload: state || true, inherit: false, notify: true}); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#go + * @methodOf ui.router.state.$state + * + * @description + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you'd like to update (while letting unspecified parameters + * inherit from the currently active ancestor states). + * + * @example + *
    +     * var app = angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.changeState = function () {
    +     *     $state.go('contact.detail');
    +     *   };
    +     * });
    +     * 
    + * + * + * @param {string} to Absolute state name or relative state path. Some examples: + * + * - `$state.go('contact.detail')` - will go to the `contact.detail` state + * - `$state.go('^')` - will go to a parent state + * - `$state.go('^.sibling')` - will go to a sibling state + * - `$state.go('.child.grandchild')` - will go to grandchild state + * + * @param {object=} params A map of the parameters that will be sent to the state, + * will populate $stateParams. Any parameters that are not specified will be inherited from currently + * defined parameters. This allows, for example, going to a sibling state that shares parameters + * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. + * transitioning to a sibling will get you the parameters for all parents, transitioning to a child + * will get you all current parameters, etc. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * + * @returns {promise} A promise representing the state of the new transition. + * + * Possible success values: + * + * - $state.current + * + *
    Possible rejection values: + * + * - 'transition superseded' - when a newer transition has been started after this one + * - 'transition prevented' - when `event.preventDefault()` has been called in a `$stateChangeStart` listener + * - 'transition aborted' - when `event.preventDefault()` has been called in a `$stateNotFound` listener or + * when a `$stateNotFound` `event.retry` promise errors. + * - 'transition failed' - when a state has been unsuccessfully found after 2 tries. + * - *resolve error* - when an error has occurred with a `resolve` + * + */ + $state.go = function go(to, params, options) { + return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#transitionTo + * @methodOf ui.router.state.$state + * + * @description + * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} + * uses `transitionTo` internally. `$state.go` is recommended in most situations. + * + * @example + *
    +     * var app = angular.module('app', ['ui.router']);
    +     *
    +     * app.controller('ctrl', function ($scope, $state) {
    +     *   $scope.changeState = function () {
    +     *     $state.transitionTo('contact.detail');
    +     *   };
    +     * });
    +     * 
    + * + * @param {string} to State name. + * @param {object=} toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false|string=|object=}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd + * use this when you want to force a reload when *everything* is the same, including search params. + * if String, then will reload the state with the name given in reload, and any children. + * if Object, then a stateObj is expected, will reload the state found in stateObj, and any children. + * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.transitionTo = function transitionTo(to, toParams, options) { + toParams = toParams || {}; + options = extend({ + location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false + }, options || {}); + + var from = $state.$current, fromParams = $state.params, fromPath = from.path; + var evt, toState = findState(to, options.relative); + + // Store the hash param for later (since it will be stripped out by various methods) + var hash = toParams['#']; + + if (!isDefined(toState)) { + var redirect = { to: to, toParams: toParams, options: options }; + var redirectResult = handleRedirect(redirect, from.self, fromParams, options); + + if (redirectResult) { + return redirectResult; + } + + // Always retry once if the $stateNotFound was not prevented + // (handles either redirect changed or state lazy-definition) + to = redirect.to; + toParams = redirect.toParams; + options = redirect.options; + toState = findState(to, options.relative); + + if (!isDefined(toState)) { + if (!options.relative) throw new Error("No such state '" + to + "'"); + throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); + } + } + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + if (!toState.params.$$validates(toParams)) return TransitionFailed; + + toParams = toState.params.$$values(toParams); + to = toState; + + var toPath = to.path; + + // Starting from the root of the path, keep all levels that haven't changed + var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + + if (!options.reload) { + while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } else if (isString(options.reload) || isObject(options.reload)) { + if (isObject(options.reload) && !options.reload.name) { + throw new Error('Invalid reload state object'); + } + + var reloadState = options.reload === true ? fromPath[0] : findState(options.reload); + if (options.reload && !reloadState) { + throw new Error("No such reload state '" + (isString(options.reload) ? options.reload : options.reload.name) + "'"); + } + + while (state && state === fromPath[keep] && state !== reloadState) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } + + // If we're going to the same state and all locals are kept, we've got nothing to do. + // But clear 'transition', as we still want to cancel any other pending transitions. + // TODO: We may not want to bump 'transition' if we're called from a location change + // that we've initiated ourselves, because we might accidentally abort a legitimate + // transition initiated from code? + if (shouldSkipReload(to, toParams, from, fromParams, locals, options)) { + if (hash) toParams['#'] = hash; + $state.params = toParams; + copy($state.params, $stateParams); + if (options.location && to.navigable && to.navigable.url) { + $urlRouter.push(to.navigable.url, toParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + $urlRouter.update(true); + } + $state.transition = null; + return $q.when($state.current); + } + + // Filter parameters before we pass them to event handlers etc. + toParams = filterByKeys(to.params.$$keys(), toParams || {}); + + // Broadcast start event and cancel the transition if requested + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeStart + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when the state transition **begins**. You can use `event.preventDefault()` + * to prevent the transition from happening and then the transition promise will be + * rejected with a `'transition prevented'` value. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * + * @example + * + *
    +         * $rootScope.$on('$stateChangeStart',
    +         * function(event, toState, toParams, fromState, fromParams){
    +         *     event.preventDefault();
    +         *     // transitionTo() promise will be rejected with
    +         *     // a 'transition prevented' error
    +         * })
    +         * 
    + */ + if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) { + $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams); + $urlRouter.update(); + return TransitionPrevented; + } + } + + // Resolve locals for the remaining states, but don't update any global state just + // yet -- if anything fails to resolve the current state needs to remain untouched. + // We also set up an inheritance chain for the locals here. This allows the view directive + // to quickly look up the correct definition for each view in the current state. Even + // though we create the locals object itself outside resolveState(), it is initially + // empty and gets filled asynchronously. We need to keep track of the promise for the + // (fully resolved) current locals, and pass this down the chain. + var resolved = $q.when(locals); + + for (var l = keep; l < toPath.length; l++, state = toPath[l]) { + locals = toLocals[l] = inherit(locals); + resolved = resolveState(state, toParams, state === to, resolved, locals, options); + } + + // Once everything is resolved, we are ready to perform the actual transition + // and return a promise for the new state. We also keep track of what the + // current promise is, so that we can detect overlapping transitions and + // keep only the outcome of the last transition. + var transition = $state.transition = resolved.then(function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit 'from' states not kept + for (l = fromPath.length - 1; l >= keep; l--) { + exiting = fromPath[l]; + if (exiting.self.onExit) { + $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + } + exiting.locals = null; + } + + // Enter 'to' states not kept + for (l = keep; l < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } + } + + // Re-add the saved hash before we start returning things + if (hash) toParams['#'] = hash; + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + $$avoidResync: true, replace: options.location === 'replace' + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams); + } + $urlRouter.update(true); + + return $state.current; + }, function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It's important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error); + + if (!evt.defaultPrevented) { + $urlRouter.update(); + } + + return $q.reject(error); + }); + + return transition; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#is + * @methodOf ui.router.state.$state + * + * @description + * Similar to {@link ui.router.state.$state#methods_includes $state.includes}, + * but only checks for the full state name. If params is supplied then it will be + * tested for strict equality against the current active params object, so all params + * must match with none missing and no extras. + * + * @example + *
    +     * $state.$current.name = 'contacts.details.item';
    +     *
    +     * // absolute name
    +     * $state.is('contact.details.item'); // returns true
    +     * $state.is(contactDetailItemStateObject); // returns true
    +     *
    +     * // relative name (. and ^), typically from a template
    +     * // E.g. from the 'contacts.details' template
    +     * 
    Item
    + *
    + * + * @param {string|object} stateOrName The state name (absolute or relative) or state object you'd like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you'd like + * to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object} - If `stateOrName` is a relative state name and `options.relative` is set, .is will + * test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it is the state. + */ + $state.is = function is(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) { return undefined; } + if ($state.$current !== state) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams) : true; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#includes + * @methodOf ui.router.state.$state + * + * @description + * A method to determine if the current active state is equal to or is the child of the + * state stateName. If any params are passed then they will be tested for a match as well. + * Not all the parameters need to be passed, just the ones you'd like to test for equality. + * + * @example + * Partial and relative names + *
    +     * $state.$current.name = 'contacts.details.item';
    +     *
    +     * // Using partial names
    +     * $state.includes("contacts"); // returns true
    +     * $state.includes("contacts.details"); // returns true
    +     * $state.includes("contacts.details.item"); // returns true
    +     * $state.includes("contacts.list"); // returns false
    +     * $state.includes("about"); // returns false
    +     *
    +     * // Using relative names (. and ^), typically from a template
    +     * // E.g. from the 'contacts.details' template
    +     * 
    Item
    + *
    + * + * Basic globbing patterns + *
    +     * $state.$current.name = 'contacts.details.item.url';
    +     *
    +     * $state.includes("*.details.*.*"); // returns true
    +     * $state.includes("*.details.**"); // returns true
    +     * $state.includes("**.item.**"); // returns true
    +     * $state.includes("*.details.item.url"); // returns true
    +     * $state.includes("*.details.*.url"); // returns true
    +     * $state.includes("*.details.*"); // returns false
    +     * $state.includes("item.**"); // returns false
    +     * 
    + * + * @param {string} stateOrName A partial name, relative name, or glob pattern + * to be searched for within the current state name. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, + * that you'd like to test against the current active state. + * @param {object=} options An options object. The options are: + * + * - **`relative`** - {string|object=} - If `stateOrName` is a relative state reference and `options.relative` is set, + * .includes will test relative to `options.relative` state (or name). + * + * @returns {boolean} Returns true if it does include the state + */ + $state.includes = function includes(stateOrName, params, options) { + options = extend({ relative: $state.$current }, options || {}); + if (isString(stateOrName) && isGlob(stateOrName)) { + if (!doesStateMatchGlob(stateOrName)) { + return false; + } + stateOrName = $state.$current.name; + } + + var state = findState(stateOrName, options.relative); + if (!isDefined(state)) { return undefined; } + if (!isDefined($state.$current.includes[state.name])) { return false; } + return params ? equalForKeys(state.params.$$values(params), $stateParams, objectKeys(params)) : true; + }; + + + /** + * @ngdoc function + * @name ui.router.state.$state#href + * @methodOf ui.router.state.$state + * + * @description + * A url generation method that returns the compiled url for the given state populated with the given params. + * + * @example + *
    +     * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
    +     * 
    + * + * @param {string|object} stateOrName The state name or state object you'd like to generate a url from. + * @param {object=} params An object of parameter values to fill the state's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the + * first parameter, then the constructed href url will be built from the first navigable ancestor (aka + * ancestor with a valid url). + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g '^'), + * defines which state to be relative from. + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} compiled state url + */ + $state.href = function href(stateOrName, params, options) { + options = extend({ + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current + }, options || {}); + + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) return null; + if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = (state && options.lossy) ? state.navigable : state; + + if (!nav || nav.url === undefined || nav.url === null) { + return null; + } + return $urlRouter.href(nav.url, filterByKeys(state.params.$$keys().concat('#'), params || {}), { + absolute: options.absolute + }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#get + * @methodOf ui.router.state.$state + * + * @description + * Returns the state configuration object for any specific state or all states. + * + * @param {string|object=} stateOrName (absolute or relative) If provided, will only get the config for + * the requested state. If not provided, returns an array of ALL state configs. + * @param {string|object=} context When stateOrName is a relative state reference, the state will be retrieved relative to context. + * @returns {Object|Array} State configuration object or array of all objects. + */ + $state.get = function (stateOrName, context) { + if (arguments.length === 0) return map(objectKeys(states), function(name) { return states[name].self; }); + var state = findState(stateOrName, context || $state.$current); + return (state && state.self) ? state.self : null; + }; + + function resolveState(state, params, paramsAreFiltered, inherited, dst, options) { + // Make a restricted $stateParams with only the parameters that apply to this state if + // necessary. In addition to being available to the controller and onEnter/onExit callbacks, + // we also need $stateParams to be available for any $injector calls we make during the + // dependency resolution process. + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(state.params.$$keys(), params); + var locals = { $stateParams: $stateParams }; + + // Resolve 'global' dependencies for the state, i.e. those not specific to a view. + // We're also including $stateParams in this; that way the parameters are restricted + // to the set that should be visible to the state, and are independent of when we update + // the global $state and $stateParams values. + dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); + var promises = [dst.resolve.then(function (globals) { + dst.globals = globals; + })]; + if (inherited) promises.push(inherited); + + function resolveViews() { + var viewsPromises = []; + + // Resolve template and dependencies for all views. + forEach(state.views, function (view, name) { + var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); + injectables.$template = [ function () { + return $view.load(name, { view: view, locals: dst.globals, params: $stateParams, notify: options.notify }) || ''; + }]; + + viewsPromises.push($resolve.resolve(injectables, dst.globals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, dst.globals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + dst[name] = result; + })); + }); + + return $q.all(viewsPromises).then(function(){ + return dst.globals; + }); + } + + // Wait for all the promises and then return the activation object + return $q.all(promises).then(resolveViews).then(function (values) { + return dst; + }); + } + + return $state; + } + + function shouldSkipReload(to, toParams, from, fromParams, locals, options) { + // Return true if there are no differences in non-search (path/object) params, false if there are differences + function nonSearchParamsEqual(fromAndToState, fromParams, toParams) { + // Identify whether all the parameters that differ between `fromParams` and `toParams` were search params. + function notSearchParam(key) { + return fromAndToState.params[key].location != "search"; + } + var nonQueryParamKeys = fromAndToState.params.$$keys().filter(notSearchParam); + var nonQueryParams = pick.apply({}, [fromAndToState.params].concat(nonQueryParamKeys)); + var nonQueryParamSet = new $$UMFP.ParamSet(nonQueryParams); + return nonQueryParamSet.$$equals(fromParams, toParams); + } + + // If reload was not explicitly requested + // and we're transitioning to the same state we're already in + // and the locals didn't change + // or they changed in a way that doesn't merit reloading + // (reloadOnParams:false, or reloadOnSearch.false and only search params changed) + // Then return true. + if (!options.reload && to === from && + (locals === from.locals || (to.self.reloadOnSearch === false && nonSearchParamsEqual(from, fromParams, toParams)))) { + return true; + } + } +} + +angular.module('ui.router.state') + .value('$stateParams', {}) + .provider('$state', $StateProvider); diff --git a/awx/ui/client/lib/angular-ui-router/src/stateDirectives.js b/awx/ui/client/lib/angular-ui-router/src/stateDirectives.js new file mode 100644 index 0000000000..09991030c2 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/stateDirectives.js @@ -0,0 +1,285 @@ +function parseStateRef(ref, current) { + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (preparsed) ref = current + '(' + preparsed[1] + ')'; + parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); + return { state: parsed[1], paramExpr: parsed[3] || null }; +} + +function stateContext(el) { + var stateData = el.parent().inheritedData('$uiView'); + + if (stateData && stateData.state && stateData.state.name) { + return stateData.state; + } +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref + * + * @requires ui.router.state.$state + * @requires $timeout + * + * @restrict A + * + * @description + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. + * + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * handled natively by the browser. + * + * You can also use relative state paths within ui-sref, just like the relative + * paths passed to `$state.go()`. You just need to be aware that the path is relative + * to the state that the link lives in, in other words the state that loaded the + * template containing the link. + * + * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} + * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, + * and `reload`. + * + * @example + * Here's an example of how you'd use ui-sref and how it would compile. If you have the + * following template: + *
    + * Home | About | Next page
    + * 
    + * 
    + * 
    + * + * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): + *
    + * Home | About | Next page
    + * 
    + * 
      + *
    • + * Joe + *
    • + *
    • + * Alice + *
    • + *
    • + * Bob + *
    • + *
    + * + * Home + *
    + * + * @param {string} ui-sref 'stateName' can be any valid absolute or relative state + * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} + */ +$StateRefDirective.$inject = ['$state', '$timeout']; +function $StateRefDirective($state, $timeout) { + var allowedOptions = ['location', 'inherit', 'reload', 'absolute']; + + return { + restrict: 'A', + require: ['?^uiSrefActive', '?^uiSrefActiveEq'], + link: function(scope, element, attrs, uiSrefActive) { + var ref = parseStateRef(attrs.uiSref, $state.current.name); + var params = null, url = null, base = stateContext(element) || $state.$current; + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var hrefKind = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? + 'xlink:href' : 'href'; + var newHref = null, isAnchor = element.prop("tagName").toUpperCase() === "A"; + var isForm = element[0].nodeName === "FORM"; + var attr = isForm ? "action" : hrefKind, nav = true; + + var options = { relative: base, inherit: true }; + var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {}; + + angular.forEach(allowedOptions, function(option) { + if (option in optionsOverride) { + options[option] = optionsOverride[option]; + } + }); + + var update = function(newVal) { + if (newVal) params = angular.copy(newVal); + if (!nav) return; + + newHref = $state.href(ref.state, params, options); + + var activeDirective = uiSrefActive[1] || uiSrefActive[0]; + if (activeDirective) { + activeDirective.$$addStateInfo(ref.state, params); + } + if (newHref === null) { + nav = false; + return false; + } + attrs.$set(attr, newHref); + }; + + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function(newVal, oldVal) { + if (newVal !== params) update(newVal); + }, true); + params = angular.copy(scope.$eval(ref.paramExpr)); + } + update(); + + if (isForm) return; + + element.bind("click", function(e) { + var button = e.which || e.button; + if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) { + // HACK: This is to allow ng-clicks to be processed before the transition is initiated: + var transition = $timeout(function() { + $state.go(ref.state, params, options); + }); + e.preventDefault(); + + // if the state has no URL, ignore one preventDefault from the directive. + var ignorePreventDefaultCount = isAnchor && !newHref ? 1: 0; + e.preventDefault = function() { + if (ignorePreventDefaultCount-- <= 0) + $timeout.cancel(transition); + }; + } + }); + } + }; +} + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * A directive working alongside ui-sref to add classes to an element when the + * related ui-sref directive's state is active, and removing them when it is inactive. + * The primary use-case is to simplify the special appearance of navigation menus + * relying on `ui-sref`, by having the "active" state's menu button appear different, + * distinguishing it from the inactive menu items. + * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * ui-sref-active found at the same level or above the ui-sref will be used. + * + * Will activate when the ui-sref's target state or any child state is active. If you + * need to activate only when the ui-sref target state is active and *not* any of + * it's children, then you will use + * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} + * + * @example + * Given the following template: + *
    + * 
    + * 
    + * + * + * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the 'active' class): + *
    + * 
    + * 
    + * + * The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * + * Multiple classes may be specified in a space-separated format: + *
    + * 
      + *
    • + * link + *
    • + *
    + *
    + */ + +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-eq + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will only activate + * when the exact target state used in the `ui-sref` is active; no child states. + * + */ +$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate']; +function $StateRefActiveDirective($state, $stateParams, $interpolate) { + return { + restrict: "A", + controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { + var states = [], activeClass; + + // There probably isn't much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope); + + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$addStateInfo = function (newState, newParams) { + var state = $state.get(newState, stateContext($element)); + + states.push({ + state: state || { name: newState }, + params: newParams + }); + + update(); + }; + + $scope.$on('$stateChangeSuccess', update); + + // Update route state + function update() { + if (anyMatch()) { + $element.addClass(activeClass); + } else { + $element.removeClass(activeClass); + } + } + + function anyMatch() { + for (var i = 0; i < states.length; i++) { + if (isMatch(states[i].state, states[i].params)) { + return true; + } + } + return false; + } + + function isMatch(state, params) { + if (typeof $attrs.uiSrefActiveEq !== 'undefined') { + return $state.is(state.name, params); + } else { + return $state.includes(state.name, params); + } + } + }] + }; +} + +angular.module('ui.router.state') + .directive('uiSref', $StateRefDirective) + .directive('uiSrefActive', $StateRefActiveDirective) + .directive('uiSrefActiveEq', $StateRefActiveDirective); diff --git a/awx/ui/client/lib/angular-ui-router/src/stateFilters.js b/awx/ui/client/lib/angular-ui-router/src/stateFilters.js new file mode 100644 index 0000000000..e0a117580a --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/stateFilters.js @@ -0,0 +1,39 @@ +/** + * @ngdoc filter + * @name ui.router.state.filter:isState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. + */ +$IsStateFilter.$inject = ['$state']; +function $IsStateFilter($state) { + var isFilter = function (state) { + return $state.is(state); + }; + isFilter.$stateful = true; + return isFilter; +} + +/** + * @ngdoc filter + * @name ui.router.state.filter:includedByState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_includes $state.includes('fullOrPartialStateName')}. + */ +$IncludedByStateFilter.$inject = ['$state']; +function $IncludedByStateFilter($state) { + var includesFilter = function (state) { + return $state.includes(state); + }; + includesFilter.$stateful = true; + return includesFilter; +} + +angular.module('ui.router.state') + .filter('isState', $IsStateFilter) + .filter('includedByState', $IncludedByStateFilter); diff --git a/awx/ui/client/lib/angular-ui-router/src/templateFactory.js b/awx/ui/client/lib/angular-ui-router/src/templateFactory.js new file mode 100644 index 0000000000..ca491a987c --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/templateFactory.js @@ -0,0 +1,110 @@ +/** + * @ngdoc object + * @name ui.router.util.$templateFactory + * + * @requires $http + * @requires $templateCache + * @requires $injector + * + * @description + * Service. Manages loading of templates. + */ +$TemplateFactory.$inject = ['$http', '$templateCache', '$injector']; +function $TemplateFactory( $http, $templateCache, $injector) { + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromConfig + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template from a configuration object. + * + * @param {object} config Configuration object for which to load a template. + * The following properties are search in the specified order, and the first one + * that is defined is used to create the template: + * + * @param {string|object} config.template html string template or function to + * load via {@link ui.router.util.$templateFactory#fromString fromString}. + * @param {string|object} config.templateUrl url to load or a function returning + * the url to load via {@link ui.router.util.$templateFactory#fromUrl fromUrl}. + * @param {Function} config.templateProvider function to invoke via + * {@link ui.router.util.$templateFactory#fromProvider fromProvider}. + * @param {object} params Parameters to pass to the template function. + * @param {object} locals Locals to pass to `invoke` if the template is loaded + * via a `templateProvider`. Defaults to `{ params: params }`. + * + * @return {string|object} The template html as a string, or a promise for + * that string,or `null` if no template is configured. + */ + this.fromConfig = function (config, params, locals) { + return ( + isDefined(config.template) ? this.fromString(config.template, params) : + isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) : + isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) : + null + ); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromString + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template from a string or a function returning a string. + * + * @param {string|object} template html template as a string or function that + * returns an html template as a string. + * @param {object} params Parameters to pass to the template function. + * + * @return {string|object} The template html as a string, or a promise for that + * string. + */ + this.fromString = function (template, params) { + return isFunction(template) ? template(params) : template; + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromUrl + * @methodOf ui.router.util.$templateFactory + * + * @description + * Loads a template from the a URL via `$http` and `$templateCache`. + * + * @param {string|Function} url url of the template to load, or a function + * that returns a url. + * @param {Object} params Parameters to pass to the url function. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + this.fromUrl = function (url, params) { + if (isFunction(url)) url = url(params); + if (url == null) return null; + else return $http + .get(url, { cache: $templateCache, headers: { Accept: 'text/html' }}) + .then(function(response) { return response.data; }); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromProvider + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template by invoking an injectable provider function. + * + * @param {Function} provider Function to invoke via `$injector.invoke` + * @param {Object} params Parameters for the template. + * @param {Object} locals Locals to pass to `invoke`. Defaults to + * `{ params: params }`. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + this.fromProvider = function (provider, params, locals) { + return $injector.invoke(provider, null, locals || { params: params }); + }; +} + +angular.module('ui.router.util').service('$templateFactory', $TemplateFactory); diff --git a/awx/ui/client/lib/angular-ui-router/src/urlMatcherFactory.js b/awx/ui/client/lib/angular-ui-router/src/urlMatcherFactory.js new file mode 100644 index 0000000000..bf116f0270 --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/urlMatcherFactory.js @@ -0,0 +1,1050 @@ +var $$UMFP; // reference to $UrlMatcherFactoryProvider + +/** + * @ngdoc object + * @name ui.router.util.type:UrlMatcher + * + * @description + * Matches URLs against patterns and extracts named parameters from the path or the search + * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list + * of search parameters. Multiple search parameter names are separated by '&'. Search parameters + * do not influence whether or not a URL is matched, but their values are passed through into + * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. + * + * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace + * syntax, which optionally allows a regular expression for the parameter to be specified: + * + * * `':'` name - colon placeholder + * * `'*'` name - catch-all placeholder + * * `'{' name '}'` - curly placeholder + * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the + * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. + * + * Parameter names may contain only word characters (latin letters, digits, and underscore) and + * must be unique within the pattern (across both path and search parameters). For colon + * placeholders or curly placeholders without an explicit regexp, a path parameter matches any + * number of characters other than '/'. For catch-all placeholders the path parameter matches + * any number of characters. + * + * Examples: + * + * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for + * trailing slashes, and patterns have to match the entire path, not just a prefix. + * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or + * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. + * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. + * * `'/user/{id:[^/]*}'` - Same as the previous example. + * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id + * parameter consists of 1 to 8 hex digits. + * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the + * path into the parameter 'path'. + * * `'/files/*path'` - ditto. + * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined + * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start + * + * @param {string} pattern The pattern to compile into a matcher. + * @param {Object} config A configuration object hash: + * @param {Object=} parentMatcher Used to concatenate the pattern/config onto + * an existing UrlMatcher + * + * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. + * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. + * + * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any + * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns + * non-null) will start with this prefix. + * + * @property {string} source The pattern that was passed into the constructor + * + * @property {string} sourcePath The path portion of the source property + * + * @property {string} sourceSearch The search portion of the source property + * + * @property {string} regex The constructed regex that will be used to match against the url when + * it is time to determine which url will match. + * + * @returns {Object} New `UrlMatcher` object + */ +function UrlMatcher(pattern, config, parentMatcher) { + config = extend({ params: {} }, isObject(config) ? config : {}); + + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: + // '*' name + // ':' name + // '{' name '}' + // '{' name ':' regexp '}' + // The regular expression is somewhat complicated due to the need to allow curly braces + // inside the regular expression. The placeholder regexp breaks down as follows: + // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) + // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case + // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + compiled = '^', last = 0, m, + segments = this.segments = [], + parentParams = parentMatcher ? parentMatcher.params : {}, + params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), + paramNames = []; + + function addParameter(id, type, config, location) { + paramNames.push(id); + if (parentParams[id]) return parentParams[id]; + if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + params[id] = new $$UMFP.Param(id, type, config, location); + return params[id]; + } + + function quoteRegExp(string, pattern, squash, optional) { + var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + switch(squash) { + case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; + case true: surroundPattern = ['?(', ')?']; break; + default: surroundPattern = ['(' + squash + "|", ')?']; break; + } + return result + surroundPattern[0] + pattern + surroundPattern[1]; + } + + this.source = pattern; + + // Split into static segments separated by path parameter placeholders. + // The number of segments is always 1 more than the number of parameters. + function matchDetails(m, isSearch) { + var id, regexp, segment, type, cfg, arrayMode; + id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null + cfg = config.params[id]; + segment = pattern.substring(last, m.index); + regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); + type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); + return { + id: id, regexp: regexp, segment: segment, type: type, cfg: cfg + }; + } + + var p, param, segment; + while ((m = placeholder.exec(pattern))) { + p = matchDetails(m, false); + if (p.segment.indexOf('?') >= 0) break; // we're into the search part + + param = addParameter(p.id, p.type, p.cfg, "path"); + compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); + segments.push(p.segment); + last = placeholder.lastIndex; + } + segment = pattern.substring(last); + + // Find any search parameter names and remove them from the last segment + var i = segment.indexOf('?'); + + if (i >= 0) { + var search = this.sourceSearch = segment.substring(i); + segment = segment.substring(0, i); + this.sourcePath = pattern.substring(0, last + i); + + if (search.length > 0) { + last = 0; + while ((m = searchPlaceholder.exec(search))) { + p = matchDetails(m, true); + param = addParameter(p.id, p.type, p.cfg, "search"); + last = placeholder.lastIndex; + // check if ?& + } + } + } else { + this.sourcePath = pattern; + this.sourceSearch = ''; + } + + compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; + segments.push(segment); + + this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); + this.prefix = segments[0]; + this.$$paramNames = paramNames; +} + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#concat + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns a new matcher for a pattern constructed by appending the path part and adding the + * search parameters of the specified pattern to this pattern. The current pattern is not + * modified. This can be understood as creating a pattern for URLs that are relative to (or + * suffixes of) the current pattern. + * + * @example + * The following two matchers are equivalent: + *
    + * new UrlMatcher('/user/{id}?q').concat('/details?date');
    + * new UrlMatcher('/user/{id}/details?q&date');
    + * 
    + * + * @param {string} pattern The pattern to append. + * @param {Object} config An object hash of the configuration for the matcher. + * @returns {UrlMatcher} A matcher for the concatenated pattern. + */ +UrlMatcher.prototype.concat = function (pattern, config) { + // Because order of search parameters is irrelevant, we can add our own search + // parameters to the end of the new pattern. Parse the new pattern by itself + // and then join the bits together, but it's much easier to do this on a string level. + var defaultConfig = { + caseInsensitive: $$UMFP.caseInsensitive(), + strict: $$UMFP.strictMode(), + squash: $$UMFP.defaultSquashPolicy() + }; + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); +}; + +UrlMatcher.prototype.toString = function () { + return this.source; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#exec + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Tests the specified path against this matcher, and returns an object containing the captured + * parameter values, or null if the path does not match. The returned object contains the values + * of any search parameters that are mentioned in the pattern, but their value may be null if + * they are not present in `searchParams`. This means that search parameters are always treated + * as optional. + * + * @example + *
    + * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
    + *   x: '1', q: 'hello'
    + * });
    + * // returns { id: 'bob', q: 'hello', r: null }
    + * 
    + * + * @param {string} path The URL path to match, e.g. `$location.path()`. + * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @returns {Object} The captured parameter values. + */ +UrlMatcher.prototype.exec = function (path, searchParams) { + var m = this.regexp.exec(path); + if (!m) return null; + searchParams = searchParams || {}; + + var paramNames = this.parameters(), nTotal = paramNames.length, + nPath = this.segments.length - 1, + values = {}, i, j, cfg, paramName; + + if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + + function decodePathArray(string) { + function reverseString(str) { return str.split("").reverse().join(""); } + function unquoteDashes(str) { return str.replace(/\\-/g, "-"); } + + var split = reverseString(string).split(/-(?!\\)/); + var allReversed = map(split, reverseString); + return map(allReversed, unquoteDashes).reverse(); + } + + for (i = 0; i < nPath; i++) { + paramName = paramNames[i]; + var param = this.params[paramName]; + var paramVal = m[i+1]; + // if the param value matches a pre-replace pair, replace the value before decoding. + for (j = 0; j < param.replace; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); + values[paramName] = param.value(paramVal); + } + for (/**/; i < nTotal; i++) { + paramName = paramNames[i]; + values[paramName] = this.params[paramName].value(searchParams[paramName]); + } + + return values; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#parameters + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns the names of all path and search parameters of this pattern in an unspecified order. + * + * @returns {Array.} An array of parameter names. Must be treated as read-only. If the + * pattern has no parameters, an empty array is returned. + */ +UrlMatcher.prototype.parameters = function (param) { + if (!isDefined(param)) return this.$$paramNames; + return this.params[param] || null; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#validate + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param {Object} params The object hash of parameters to validate. + * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. + */ +UrlMatcher.prototype.validates = function (params) { + return this.params.$$validates(params); +}; + +/** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#format + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Creates a URL that matches this pattern by substituting the specified values + * for the path and search parameters. Null values for path parameters are + * treated as empty strings. + * + * @example + *
    + * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
    + * // returns '/user/bob?q=yes'
    + * 
    + * + * @param {Object} values the values to substitute for the parameters in this pattern. + * @returns {string} the formatted URL (path and optionally search part). + */ +UrlMatcher.prototype.format = function (values) { + values = values || {}; + var segments = this.segments, params = this.parameters(), paramset = this.params; + if (!this.validates(values)) return null; + + var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; + + function encodeDashes(str) { // Replace dashes with encoded "\-" + return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); + } + + for (i = 0; i < nTotal; i++) { + var isPathParam = i < nPath; + var name = params[i], param = paramset[name], value = param.value(values[name]); + var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); + var squash = isDefaultValue ? param.squash : false; + var encoded = param.type.encode(value); + + if (isPathParam) { + var nextSegment = segments[i + 1]; + if (squash === false) { + if (encoded != null) { + if (isArray(encoded)) { + result += map(encoded, encodeDashes).join("-"); + } else { + result += encodeURIComponent(encoded); + } + } + result += nextSegment; + } else if (squash === true) { + var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; + result += nextSegment.match(capture)[1]; + } else if (isString(squash)) { + result += squash + nextSegment; + } + } else { + if (encoded == null || (isDefaultValue && squash !== false)) continue; + if (!isArray(encoded)) encoded = [ encoded ]; + encoded = map(encoded, encodeURIComponent).join('&' + name + '='); + result += (search ? '&' : '?') + (name + '=' + encoded); + search = true; + } + } + + return result; +}; + +/** + * @ngdoc object + * @name ui.router.util.type:Type + * + * @description + * Implements an interface to define custom parameter types that can be decoded from and encoded to + * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} + * objects when matching or formatting URLs, or comparing or validating parameter values. + * + * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more + * information on registering custom types. + * + * @param {Object} config A configuration object which contains the custom type definition. The object's + * properties will override the default methods and/or pattern in `Type`'s public interface. + * @example + *
    + * {
    + *   decode: function(val) { return parseInt(val, 10); },
    + *   encode: function(val) { return val && val.toString(); },
    + *   equals: function(a, b) { return this.is(a) && a === b; },
    + *   is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
    + *   pattern: /\d+/
    + * }
    + * 
    + * + * @property {RegExp} pattern The regular expression pattern used to match values of this type when + * coming from a substring of a URL. + * + * @returns {Object} Returns a new `Type` object. + */ +function Type(config) { + extend(this, config); +} + +/** + * @ngdoc function + * @name ui.router.util.type:Type#is + * @methodOf ui.router.util.type:Type + * + * @description + * Detects whether a value is of a particular type. Accepts a native (decoded) value + * and determines whether it matches the current `Type` object. + * + * @param {*} val The value to check. + * @param {string} key Optional. If the type check is happening in the context of a specific + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the + * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. + * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. + */ +Type.prototype.is = function(val, key) { + return true; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#encode + * @methodOf ui.router.util.type:Type + * + * @description + * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the + * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it + * only needs to be a representation of `val` that has been coerced to a string. + * + * @param {*} val The value to encode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {string} Returns a string representation of `val` that can be encoded in a URL. + */ +Type.prototype.encode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#decode + * @methodOf ui.router.util.type:Type + * + * @description + * Converts a parameter value (from URL string or transition param) to a custom/native value. + * + * @param {string} val The URL parameter value to decode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {*} Returns a custom representation of the URL parameter value. + */ +Type.prototype.decode = function(val, key) { + return val; +}; + +/** + * @ngdoc function + * @name ui.router.util.type:Type#equals + * @methodOf ui.router.util.type:Type + * + * @description + * Determines whether two decoded values are equivalent. + * + * @param {*} a A value to compare against. + * @param {*} b A value to compare against. + * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. + */ +Type.prototype.equals = function(a, b) { + return a == b; +}; + +Type.prototype.$subPattern = function() { + var sub = this.pattern.toString(); + return sub.substr(1, sub.length - 2); +}; + +Type.prototype.pattern = /.*/; + +Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; + +/** Given an encoded string, or a decoded object, returns a decoded object */ +Type.prototype.$normalize = function(val) { + return this.is(val) ? val : this.decode(val); +}; + +/* + * Wraps an existing custom Type as an array of Type, depending on 'mode'. + * e.g.: + * - urlmatcher pattern "/path?{queryParam[]:int}" + * - url: "/path?queryParam=1&queryParam=2 + * - $stateParams.queryParam will be [1, 2] + * if `mode` is "auto", then + * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 + * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] + */ +Type.prototype.$asArray = function(mode, isSearch) { + if (!mode) return this; + if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); + + function ArrayType(type, mode) { + function bindTo(type, callbackName) { + return function() { + return type[callbackName].apply(type, arguments); + }; + } + + // Wrap non-array value as array + function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } + // Unwrap array value for "auto" mode. Return undefined for empty array. + function arrayUnwrap(val) { + switch(val.length) { + case 0: return undefined; + case 1: return mode === "auto" ? val[0] : val; + default: return val; + } + } + function falsey(val) { return !val; } + + // Wraps type (.is/.encode/.decode) functions to operate on each value of an array + function arrayHandler(callback, allTruthyMode) { + return function handleArray(val) { + val = arrayWrap(val); + var result = map(val, callback); + if (allTruthyMode === true) + return filter(result, falsey).length === 0; + return arrayUnwrap(result); + }; + } + + // Wraps type (.equals) functions to operate on each value of an array + function arrayEqualsHandler(callback) { + return function handleArray(val1, val2) { + var left = arrayWrap(val1), right = arrayWrap(val2); + if (left.length !== right.length) return false; + for (var i = 0; i < left.length; i++) { + if (!callback(left[i], right[i])) return false; + } + return true; + }; + } + + this.encode = arrayHandler(bindTo(type, 'encode')); + this.decode = arrayHandler(bindTo(type, 'decode')); + this.is = arrayHandler(bindTo(type, 'is'), true); + this.equals = arrayEqualsHandler(bindTo(type, 'equals')); + this.pattern = type.pattern; + this.$normalize = arrayHandler(bindTo(type, '$normalize')); + this.name = type.name; + this.$arrayMode = mode; + } + + return new ArrayType(this, mode); +}; + + + +/** + * @ngdoc object + * @name ui.router.util.$urlMatcherFactory + * + * @description + * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory + * is also available to providers under the name `$urlMatcherFactoryProvider`. + */ +function $UrlMatcherFactory() { + $$UMFP = this; + + var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; + + function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } + function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } + + var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { + string: { + encode: valToString, + decode: valFromString, + // TODO: in 1.0, make string .is() return false if value is undefined/null by default. + // In 0.2.x, string params are optional by default for backwards compat + is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; }, + pattern: /[^/]*/ + }, + int: { + encode: valToString, + decode: function(val) { return parseInt(val, 10); }, + is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, + pattern: /\d+/ + }, + bool: { + encode: function(val) { return val ? 1 : 0; }, + decode: function(val) { return parseInt(val, 10) !== 0; }, + is: function(val) { return val === true || val === false; }, + pattern: /0|1/ + }, + date: { + encode: function (val) { + if (!this.is(val)) + return undefined; + return [ val.getFullYear(), + ('0' + (val.getMonth() + 1)).slice(-2), + ('0' + val.getDate()).slice(-2) + ].join("-"); + }, + decode: function (val) { + if (this.is(val)) return val; + var match = this.capture.exec(val); + return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; + }, + is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, + equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, + capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ + }, + json: { + encode: angular.toJson, + decode: angular.fromJson, + is: angular.isObject, + equals: angular.equals, + pattern: /[^/]*/ + }, + any: { // does not encode/decode + encode: angular.identity, + decode: angular.identity, + equals: angular.equals, + pattern: /.*/ + } + }; + + function getDefaultConfig() { + return { + strict: isStrictMode, + caseInsensitive: isCaseInsensitive + }; + } + + function isInjectable(value) { + return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + $UrlMatcherFactory.$$getDefaultValue = function(config) { + if (!isInjectable(config.value)) return config.value; + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(config.value); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#caseInsensitive + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URL matching should be case sensitive (the default behavior), or not. + * + * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + * @returns {boolean} the current value of caseInsensitive + */ + this.caseInsensitive = function(value) { + if (isDefined(value)) + isCaseInsensitive = value; + return isCaseInsensitive; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#strictMode + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URLs should match trailing slashes, or not (the default behavior). + * + * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. + * @returns {boolean} the current value of strictMode + */ + this.strictMode = function(value) { + if (isDefined(value)) + isStrictMode = value; + return isStrictMode; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Sets the default behavior when generating or matching URLs with default parameter values. + * + * @param {string} value A string that defines the default parameter URL squashing behavior. + * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL + * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the + * parameter is surrounded by slashes, squash (remove) one slash from the URL + * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) + * the parameter value from the URL and replace it with this string. + */ + this.defaultSquashPolicy = function(value) { + if (!isDefined(value)) return defaultSquashPolicy; + if (value !== true && value !== false && !isString(value)) + throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); + defaultSquashPolicy = value; + return value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#compile + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. + * + * @param {string} pattern The URL pattern. + * @param {Object} config The config object hash. + * @returns {UrlMatcher} The UrlMatcher. + */ + this.compile = function (pattern, config) { + return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#isMatcher + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Returns true if the specified object is a `UrlMatcher`, or false otherwise. + * + * @param {Object} object The object to perform the type check against. + * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + this.isMatcher = function (o) { + if (!isObject(o)) return false; + var result = true; + + forEach(UrlMatcher.prototype, function(val, name) { + if (isFunction(val)) { + result = result && (isDefined(o[name]) && isFunction(o[name])); + } + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#type + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to + * generate URLs with typed parameters. + * + * @param {string} name The type name. + * @param {Object|Function} definition The type definition. See + * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * @param {Object|Function} definitionFn (optional) A function that is injected before the app + * runtime starts. The result of this function is merged into the existing `definition`. + * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * + * @returns {Object} Returns `$urlMatcherFactoryProvider`. + * + * @example + * This is a simple example of a custom type that encodes and decodes items from an + * array, using the array index as the URL-encoded value: + * + *
    +   * var list = ['John', 'Paul', 'George', 'Ringo'];
    +   *
    +   * $urlMatcherFactoryProvider.type('listItem', {
    +   *   encode: function(item) {
    +   *     // Represent the list item in the URL using its corresponding index
    +   *     return list.indexOf(item);
    +   *   },
    +   *   decode: function(item) {
    +   *     // Look up the list item by index
    +   *     return list[parseInt(item, 10)];
    +   *   },
    +   *   is: function(item) {
    +   *     // Ensure the item is valid by checking to see that it appears
    +   *     // in the list
    +   *     return list.indexOf(item) > -1;
    +   *   }
    +   * });
    +   *
    +   * $stateProvider.state('list', {
    +   *   url: "/list/{item:listItem}",
    +   *   controller: function($scope, $stateParams) {
    +   *     console.log($stateParams.item);
    +   *   }
    +   * });
    +   *
    +   * // ...
    +   *
    +   * // Changes URL to '/list/3', logs "Ringo" to the console
    +   * $state.go('list', { item: "Ringo" });
    +   * 
    + * + * This is a more complex example of a type that relies on dependency injection to + * interact with services, and uses the parameter name from the URL to infer how to + * handle encoding and decoding parameter values: + * + *
    +   * // Defines a custom type that gets a value from a service,
    +   * // where each service gets different types of values from
    +   * // a backend API:
    +   * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
    +   *
    +   *   // Matches up services to URL parameter names
    +   *   var services = {
    +   *     user: Users,
    +   *     post: Posts
    +   *   };
    +   *
    +   *   return {
    +   *     encode: function(object) {
    +   *       // Represent the object in the URL using its unique ID
    +   *       return object.id;
    +   *     },
    +   *     decode: function(value, key) {
    +   *       // Look up the object by ID, using the parameter
    +   *       // name (key) to call the correct service
    +   *       return services[key].findById(value);
    +   *     },
    +   *     is: function(object, key) {
    +   *       // Check that object is a valid dbObject
    +   *       return angular.isObject(object) && object.id && services[key];
    +   *     }
    +   *     equals: function(a, b) {
    +   *       // Check the equality of decoded objects by comparing
    +   *       // their unique IDs
    +   *       return a.id === b.id;
    +   *     }
    +   *   };
    +   * });
    +   *
    +   * // In a config() block, you can then attach URLs with
    +   * // type-annotated parameters:
    +   * $stateProvider.state('users', {
    +   *   url: "/users",
    +   *   // ...
    +   * }).state('users.item', {
    +   *   url: "/{user:dbObject}",
    +   *   controller: function($scope, $stateParams) {
    +   *     // $stateParams.user will now be an object returned from
    +   *     // the Users service
    +   *   },
    +   *   // ...
    +   * });
    +   * 
    + */ + this.type = function (name, definition, definitionFn) { + if (!isDefined(definition)) return $types[name]; + if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); + + $types[name] = new Type(extend({ name: name }, definition)); + if (definitionFn) { + typeQueue.push({ name: name, def: definitionFn }); + if (!enqueue) flushTypeQueue(); + } + return this; + }; + + // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s + function flushTypeQueue() { + while(typeQueue.length) { + var type = typeQueue.shift(); + if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); + angular.extend($types[type.name], injector.invoke(type.def)); + } + } + + // Register default types. Store them in the prototype of $types. + forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); + $types = inherit($types, {}); + + /* No need to document $get, since it returns this */ + this.$get = ['$injector', function ($injector) { + injector = $injector; + enqueue = false; + flushTypeQueue(); + + forEach(defaultTypes, function(type, name) { + if (!$types[name]) $types[name] = new Type(type); + }); + return this; + }]; + + this.Param = function Param(id, type, config, location) { + var self = this; + config = unwrapShorthand(config); + type = getType(config, type, location); + var arrayMode = getArrayMode(); + type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; + if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) + config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" + var isOptional = config.value !== undefined; + var squash = getSquashPolicy(config, isOptional); + var replace = getReplace(config, arrayMode, isOptional, squash); + + function unwrapShorthand(config) { + var keys = isObject(config) ? objectKeys(config) : []; + var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && + indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; + if (isShorthand) config = { value: config }; + config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; + return config; + } + + function getType(config, urlType, location) { + if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); + if (urlType) return urlType; + if (!config.type) return (location === "config" ? $types.any : $types.string); + return config.type instanceof Type ? config.type : new Type(config.type); + } + + // array config: param name (param[]) overrides default settings. explicit config overrides param name. + function getArrayMode() { + var arrayDefaults = { array: (location === "search" ? "auto" : false) }; + var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; + return extend(arrayDefaults, arrayParamNomenclature, config).array; + } + + /** + * returns false, true, or the squash value to indicate the "default parameter url squash policy". + */ + function getSquashPolicy(config, isOptional) { + var squash = config.squash; + if (!isOptional || squash === false) return false; + if (!isDefined(squash) || squash == null) return defaultSquashPolicy; + if (squash === true || isString(squash)) return squash; + throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); + } + + function getReplace(config, arrayMode, isOptional, squash) { + var replace, configuredKeys, defaultPolicy = [ + { from: "", to: (isOptional || arrayMode ? undefined : "") }, + { from: null, to: (isOptional || arrayMode ? undefined : "") } + ]; + replace = isArray(config.replace) ? config.replace : []; + if (isString(squash)) + replace.push({ from: squash, to: undefined }); + configuredKeys = map(replace, function(item) { return item.from; } ); + return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + function $$getDefaultValue() { + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + var defaultValue = injector.invoke(config.$$fn); + if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue)) + throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")"); + return defaultValue; + } + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } + function $replace(value) { + var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); + return replacement.length ? replacement[0] : value; + } + value = $replace(value); + return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value); + } + + function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } + + extend(this, { + id: id, + type: type, + location: location, + array: arrayMode, + squash: squash, + replace: replace, + isOptional: isOptional, + value: $value, + dynamic: undefined, + config: config, + toString: toString + }); + }; + + function ParamSet(params) { + extend(this, params || {}); + } + + ParamSet.prototype = { + $$new: function() { + return inherit(this, extend(new ParamSet(), { $$parent: this})); + }, + $$keys: function () { + var keys = [], chain = [], parent = this, + ignore = objectKeys(ParamSet.prototype); + while (parent) { chain.push(parent); parent = parent.$$parent; } + chain.reverse(); + forEach(chain, function(paramset) { + forEach(objectKeys(paramset), function(key) { + if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); + }); + }); + return keys; + }, + $$values: function(paramValues) { + var values = {}, self = this; + forEach(self.$$keys(), function(key) { + values[key] = self[key].value(paramValues && paramValues[key]); + }); + return values; + }, + $$equals: function(paramValues1, paramValues2) { + var equal = true, self = this; + forEach(self.$$keys(), function(key) { + var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; + if (!self[key].type.equals(left, right)) equal = false; + }); + return equal; + }, + $$validates: function $$validate(paramValues) { + var keys = this.$$keys(), i, param, rawVal, normalized, encoded; + for (i = 0; i < keys.length; i++) { + param = this[keys[i]]; + rawVal = paramValues[keys[i]]; + if ((rawVal === undefined || rawVal === null) && param.isOptional) + break; // There was no parameter value, but the param is optional + normalized = param.type.$normalize(rawVal); + if (!param.type.is(normalized)) + return false; // The value was not of the correct Type, and could not be decoded to the correct Type + encoded = param.type.encode(normalized); + if (angular.isString(encoded) && !param.type.pattern.exec(encoded)) + return false; // The value was of the correct type, but when encoded, did not match the Type's regexp + } + return true; + }, + $$parent: undefined + }; + + this.ParamSet = ParamSet; +} + +// Register as a provider so it's available to other providers +angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); +angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]); diff --git a/awx/ui/client/lib/angular-ui-router/src/urlRouter.js b/awx/ui/client/lib/angular-ui-router/src/urlRouter.js new file mode 100644 index 0000000000..33c17090fe --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/urlRouter.js @@ -0,0 +1,427 @@ +/** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider + * + * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider + * + * @description + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * a url in a state configuration. All urls are compiled into a UrlMatcher object. + * + * There are several methods on `$urlRouterProvider` that make it useful to use directly + * in your module config. + */ +$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider']; +function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { + var rules = [], otherwise = null, interceptDeferred = false, listener; + + // Returns a string that is a prefix of all strings matching the RegExp + function regExpPrefix(re) { + var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); + return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ''; + } + + // Interpolates matched values into a String.replace()-style pattern + function interpolate(pattern, match) { + return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === '$' ? 0 : Number(what)]; + }); + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#rule + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines rules that are used by `$urlRouterProvider` to find matches for + * specific URLs. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   // Here's an example of how you might allow case insensitive urls
    +   *   $urlRouterProvider.rule(function ($injector, $location) {
    +   *     var path = $location.path(),
    +   *         normalized = path.toLowerCase();
    +   *
    +   *     if (path !== normalized) {
    +   *       return normalized;
    +   *     }
    +   *   });
    +   * });
    +   * 
    + * + * @param {object} rule Handler function that takes `$injector` and `$location` + * services as arguments. You can use them to return a valid path as a string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.rule = function (rule) { + if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rules.push(rule); + return this; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider#otherwise + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines a path that is used when an invalid route is requested. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   // if the path doesn't match any of the urls you configured
    +   *   // otherwise will take care of routing the user to the
    +   *   // specified url
    +   *   $urlRouterProvider.otherwise('/index');
    +   *
    +   *   // Example of using function rule as param
    +   *   $urlRouterProvider.otherwise(function ($injector, $location) {
    +   *     return '/a/valid/url';
    +   *   });
    +   * });
    +   * 
    + * + * @param {string|object} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: + * `$injector` and `$location` services, and must return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.otherwise = function (rule) { + if (isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + otherwise = rule; + return this; + }; + + + function handleIfMatch($injector, handler, match) { + if (!match) return false; + var result = $injector.invoke(handler, handler, { $match: match }); + return isDefined(result) ? result : true; + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#when + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Registers a handler for a given url matching. if handle is a string, it is + * treated as a redirect, and is interpolated according to the syntax of match + * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). + * + * If the handler is a function, it is injectable. It gets invoked if `$location` + * matches. You have the option of inject the match object as `$match`. + * + * The handler can return + * + * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter` + * will continue trying to find another one that matches. + * - **string** which is treated as a redirect and passed to `$location.url()` + * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *   $urlRouterProvider.when($state.url, function ($match, $stateParams) {
    +   *     if ($state.$current.navigable !== state ||
    +   *         !equalForKeys($match, $stateParams) {
    +   *      $state.transitionTo(state, $match, false);
    +   *     }
    +   *   });
    +   * });
    +   * 
    + * + * @param {string|object} what The incoming path that you want to redirect. + * @param {string|object} handler The path you want to redirect your user to. + */ + this.when = function (what, handler) { + var redirect, handlerIsString = isString(handler); + if (isString(what)) what = $urlMatcherFactory.compile(what); + + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) + throw new Error("invalid 'handler' in when()"); + + var strategies = { + matcher: function (what, handler) { + if (handlerIsString) { + redirect = $urlMatcherFactory.compile(handler); + handler = ['$match', function ($match) { return redirect.format($match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); + }, { + prefix: isString(what.prefix) ? what.prefix : '' + }); + }, + regex: function (what, handler) { + if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); + + if (handlerIsString) { + redirect = handler; + handler = ['$match', function ($match) { return interpolate(redirect, $match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, { + prefix: regExpPrefix(what) + }); + } + }; + + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + + for (var n in check) { + if (check[n]) return this.rule(strategies[n](what, handler)); + } + + throw new Error("invalid 'what' in when()"); + }; + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#deferIntercept + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Disables (or enables) deferring location change interception. + * + * If you wish to customize the behavior of syncing the URL (for example, if you wish to + * defer a transition but maintain the current URL), call this method at configuration time. + * Then, at run time, call `$urlRouter.listen()` after you have configured your own + * `$locationChangeSuccess` event handler. + * + * @example + *
    +   * var app = angular.module('app', ['ui.router.router']);
    +   *
    +   * app.config(function ($urlRouterProvider) {
    +   *
    +   *   // Prevent $urlRouter from automatically intercepting URL changes;
    +   *   // this allows you to configure custom behavior in between
    +   *   // location changes and route synchronization:
    +   *   $urlRouterProvider.deferIntercept();
    +   *
    +   * }).run(function ($rootScope, $urlRouter, UserService) {
    +   *
    +   *   $rootScope.$on('$locationChangeSuccess', function(e) {
    +   *     // UserService is an example service for managing user state
    +   *     if (UserService.isLoggedIn()) return;
    +   *
    +   *     // Prevent $urlRouter's default handler from firing
    +   *     e.preventDefault();
    +   *
    +   *     UserService.handleLogin().then(function() {
    +   *       // Once the user has logged in, sync the current URL
    +   *       // to the router:
    +   *       $urlRouter.sync();
    +   *     });
    +   *   });
    +   *
    +   *   // Configures $urlRouter's listener *after* your custom listener
    +   *   $urlRouter.listen();
    +   * });
    +   * 
    + * + * @param {boolean} defer Indicates whether to defer location change interception. Passing + no parameter is equivalent to `true`. + */ + this.deferIntercept = function (defer) { + if (defer === undefined) defer = true; + interceptDeferred = defer; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouter + * + * @requires $location + * @requires $rootScope + * @requires $injector + * @requires $browser + * + * @description + * + */ + this.$get = $get; + $get.$inject = ['$location', '$rootScope', '$injector', '$browser']; + function $get( $location, $rootScope, $injector, $browser) { + + var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl; + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === '/') return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl; + lastPushedUrl = undefined; + // TODO: Re-implement this in 1.0 for https://github.com/angular-ui/ui-router/issues/1573 + //if (ignoreUpdate) return true; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) return false; + if (isString(handled)) $location.replace().url(handled); + return true; + } + var n = rules.length, i; + + for (i = 0; i < n; i++) { + if (check(rules[i])) return; + } + // always check otherwise last to allow dynamic updates to the set of rules + if (otherwise) check(otherwise); + } + + function listen() { + listener = listener || $rootScope.$on('$locationChangeSuccess', update); + return listener; + } + + if (!interceptDeferred) listen(); + + return { + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#sync + * @methodOf ui.router.router.$urlRouter + * + * @description + * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. + * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, + * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed + * with the transition by calling `$urlRouter.sync()`. + * + * @example + *
    +       * angular.module('app', ['ui.router'])
    +       *   .run(function($rootScope, $urlRouter) {
    +       *     $rootScope.$on('$locationChangeSuccess', function(evt) {
    +       *       // Halt state change from even starting
    +       *       evt.preventDefault();
    +       *       // Perform custom logic
    +       *       var meetsRequirement = ...
    +       *       // Continue with the update and state transition if logic allows
    +       *       if (meetsRequirement) $urlRouter.sync();
    +       *     });
    +       * });
    +       * 
    + */ + sync: function() { + update(); + }, + + listen: function() { + return listen(); + }, + + update: function(read) { + if (read) { + location = $location.url(); + return; + } + if ($location.url() === location) return; + + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + var url = urlMatcher.format(params || {}); + + // Handle the special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + $location.url(url); + lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined; + if (options && options.replace) $location.replace(); + }, + + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#href + * @methodOf ui.router.router.$urlRouter + * + * @description + * A URL generation method that returns the compiled URL for a given + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters. + * + * @example + *
    +       * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
    +       *   person: "bob"
    +       * });
    +       * // $bob == "/about/bob";
    +       * 
    + * + * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate. + * @param {object=} params An object of parameter values to fill the matcher's required parameters. + * @param {object=} options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + href: function(urlMatcher, params, options) { + if (!urlMatcher.validates(params)) return null; + + var isHtml5 = $locationProvider.html5Mode(); + if (angular.isObject(isHtml5)) { + isHtml5 = isHtml5.enabled; + } + + var url = urlMatcher.format(params); + options = options || {}; + + if (!isHtml5 && url !== null) { + url = "#" + $locationProvider.hashPrefix() + url; + } + + // Handle special hash param, if needed + if (url !== null && params && params['#']) { + url += '#' + params['#']; + } + + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? '/' : ''), port = $location.port(); + port = (port === 80 || port === 443 ? '' : ':' + port); + + return [$location.protocol(), '://', $location.host(), port, slash, url].join(''); + } + }; + } +} + +angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider); diff --git a/awx/ui/client/lib/angular-ui-router/src/view.js b/awx/ui/client/lib/angular-ui-router/src/view.js new file mode 100644 index 0000000000..f19a3c569e --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/view.js @@ -0,0 +1,71 @@ + +$ViewProvider.$inject = []; +function $ViewProvider() { + + this.$get = $get; + /** + * @ngdoc object + * @name ui.router.state.$view + * + * @requires ui.router.util.$templateFactory + * @requires $rootScope + * + * @description + * + */ + $get.$inject = ['$rootScope', '$templateFactory']; + function $get( $rootScope, $templateFactory) { + return { + // $view.load('full.viewName', { template: ..., controller: ..., resolve: ..., async: false, params: ... }) + /** + * @ngdoc function + * @name ui.router.state.$view#load + * @methodOf ui.router.state.$view + * + * @description + * + * @param {string} name name + * @param {object} options option object. + */ + load: function load(name, options) { + var result, defaults = { + template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} + }; + options = extend(defaults, options); + + if (options.view) { + result = $templateFactory.fromConfig(options.view, options.params, options.locals); + } + if (result && options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$viewContentLoading + * @eventOf ui.router.state.$view + * @eventType broadcast on root scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {Object} viewConfig The view config properties (template, controller, etc). + * + * @example + * + *
    +         * $scope.$on('$viewContentLoading',
    +         * function(event, viewConfig){
    +         *     // Access to all the view config properties.
    +         *     // and one special property 'targetView'
    +         *     // viewConfig.targetView
    +         * });
    +         * 
    + */ + $rootScope.$broadcast('$viewContentLoading', options); + } + return result; + } + }; + } +} + +angular.module('ui.router.state').provider('$view', $ViewProvider); diff --git a/awx/ui/client/lib/angular-ui-router/src/viewDirective.js b/awx/ui/client/lib/angular-ui-router/src/viewDirective.js new file mode 100644 index 0000000000..b70220779b --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/viewDirective.js @@ -0,0 +1,303 @@ +/** + * @ngdoc directive + * @name ui.router.state.directive:ui-view + * + * @requires ui.router.state.$state + * @requires $compile + * @requires $controller + * @requires $injector + * @requires ui.router.state.$uiViewScroll + * @requires $document + * + * @restrict ECA + * + * @description + * The ui-view directive tells $state where to place your templates. + * + * @param {string=} name A view name. The name should be unique amongst the other views in the + * same state. You can have views of the same name that live in different states. + * + * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window + * when a view is populated. By default, $anchorScroll is overridden by ui-router's custom scroll + * service, {@link ui.router.state.$uiViewScroll}. This custom service let's you + * scroll ui-view elements into view when they are populated during a state activation. + * + * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) + * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* + * + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @example + * A view can be unnamed or named. + *
    + * 
    + * 
    + * + * + *
    + *
    + * + * You can only have one unnamed view within any template (or root html). If you are only using a + * single view and it is unnamed then you can populate it like so: + *
    + * 
    + * $stateProvider.state("home", { + * template: "

    HELLO!

    " + * }) + *
    + * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} + * config property, by name, in this case an empty name: + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "": {
    + *       template: "

    HELLO!

    " + * } + * } + * }) + *
    + * + * But typically you'll only use the views property if you name your view or have more than one view + * in the same template. There's not really a compelling reason to name a view if its the only one, + * but you could if you wanted, like so: + *
    + * 
    + *
    + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "main": {
    + *       template: "

    HELLO!

    " + * } + * } + * }) + *
    + * + * Really though, you'll use views to set up multiple views: + *
    + * 
    + *
    + *
    + *
    + * + *
    + * $stateProvider.state("home", {
    + *   views: {
    + *     "": {
    + *       template: "

    HELLO!

    " + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + *
    + * + * Examples for `autoscroll`: + * + *
    + * 
    + * 
    + *
    + * 
    + * 
    + * 
    + * 
    + * 
    + */ +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll', '$interpolate']; +function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate) { + + function getService() { + return ($injector.has) ? function(service) { + return $injector.has(service) ? $injector.get(service) : null; + } : function(service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; + } + + var service = getService(), + $animator = service('$animator'), + $animate = service('$animate'); + + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { + var statics = function() { + return { + enter: function (element, target, cb) { target.after(element); cb(); }, + leave: function (element, cb) { element.remove(); cb(); } + }; + }; + + if ($animate) { + return { + enter: function(element, target, cb) { + var promise = $animate.enter(element, null, target, cb); + if (promise && promise.then) promise.then(cb); + }, + leave: function(element, cb) { + var promise = $animate.leave(element, cb); + if (promise && promise.then) promise.then(cb); + } + }; + } + + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return { + enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, + leave: function(element, cb) { animate.leave(element); cb(); } + }; + } + + return statics(); + } + + var directive = { + restrict: 'ECA', + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, latestLocals, + onloadExp = attrs.onload || '', + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope); + + scope.$on('$stateChangeSuccess', function() { + updateView(false); + }); + scope.$on('$viewContentLoading', function() { + updateView(false); + }); + + updateView(true); + + function cleanupLastView() { + if (previousEl) { + previousEl.remove(); + previousEl = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentEl) { + renderer.leave(currentEl, function() { + previousEl = null; + }); + + previousEl = currentEl; + currentEl = null; + } + } + + function updateView(firstTime) { + var newScope, + name = getUiViewName(scope, attrs, $element, $interpolate), + previousLocals = name && $state.$current && $state.$current.locals[name]; + + if (!firstTime && previousLocals === latestLocals) return; // nothing to do + newScope = scope.$new(); + latestLocals = $state.$current.locals[name]; + + var clone = $transclude(newScope, function(clone) { + renderer.enter(clone, $element, function onUiViewEnter() { + if(currentScope) { + currentScope.$emit('$viewContentAnimationEnded'); + } + + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + + currentEl = clone; + currentScope = newScope; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoaded + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description * + * Fired once the view is **loaded**, *after* the DOM is rendered. + * + * @param {Object} event Event object. + */ + currentScope.$emit('$viewContentLoaded'); + currentScope.$eval(onloadExp); + } + }; + } + }; + + return directive; +} + +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state', '$interpolate']; +function $ViewDirectiveFill ( $compile, $controller, $state, $interpolate) { + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + return function (scope, $element, attrs) { + var current = $state.$current, + name = getUiViewName(scope, attrs, $element, $interpolate), + locals = current && current.locals[name]; + + if (! locals) { + return; + } + + $element.data('$uiView', { name: name, state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + locals.$element = $element; + var controller = $controller(locals.$$controller, locals); + if (locals.$$controllerAs) { + scope[locals.$$controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + }; + } + }; +} + +/** + * Shared ui-view code for both directives: + * Given scope, element, and its attributes, return the view's name + */ +function getUiViewName(scope, attrs, element, $interpolate) { + var name = $interpolate(attrs.uiView || attrs.name || '')(scope); + var inherited = element.inheritedData('$uiView'); + return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); +} + +angular.module('ui.router.state').directive('uiView', $ViewDirective); +angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); diff --git a/awx/ui/client/lib/angular-ui-router/src/viewScroll.js b/awx/ui/client/lib/angular-ui-router/src/viewScroll.js new file mode 100644 index 0000000000..81114e20de --- /dev/null +++ b/awx/ui/client/lib/angular-ui-router/src/viewScroll.js @@ -0,0 +1,52 @@ +/** + * @ngdoc object + * @name ui.router.state.$uiViewScrollProvider + * + * @description + * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. + */ +function $ViewScrollProvider() { + + var useAnchorScroll = false; + + /** + * @ngdoc function + * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll + * @methodOf ui.router.state.$uiViewScrollProvider + * + * @description + * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for + * scrolling based on the url anchor. + */ + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScroll + * + * @requires $anchorScroll + * @requires $timeout + * + * @description + * When called with a jqLite element, it scrolls the element into view (after a + * `$timeout` so the DOM has time to refresh). + * + * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, + * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. + */ + this.$get = ['$anchorScroll', '$timeout', function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + return $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; +} + +angular.module('ui.router.state').provider('$uiViewScroll', $ViewScrollProvider); diff --git a/awx/ui/client/src/adhoc/adhoc.controller.js b/awx/ui/client/src/adhoc/adhoc.controller.js index 28a692a0df..24a289ebe9 100644 --- a/awx/ui/client/src/adhoc/adhoc.controller.js +++ b/awx/ui/client/src/adhoc/adhoc.controller.js @@ -9,7 +9,7 @@ * @name controllers.function:Adhoc * @description This controller controls the adhoc form creation, command launching and navigating to standard out after command has been succesfully ran. */ -function adhocController($q, $scope, $rootScope, $location, $routeParams, +function adhocController($q, $scope, $rootScope, $location, $stateParams, CheckPasswords, PromptForPasswords, CreateLaunchDialog, adhocForm, GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices, KindChange, LookUpInit, CredentialList, Empty, Wait) { @@ -31,7 +31,7 @@ function adhocController($q, $scope, $rootScope, $location, $routeParams, }; }; - var id = $routeParams.inventory_id, + var id = $stateParams.inventory_id, urls = privateFn.setAvailableUrls(), hostPattern = $rootScope.hostPatterns || "all"; @@ -72,23 +72,6 @@ function adhocController($q, $scope, $rootScope, $location, $routeParams, } }; - privateFn.getInventoryNameForBreadcrumbs = function(url) { - - Rest.setUrl(url); - var promise = Rest.get(); - promise.then(function (response) { - $scope.inv_name = response.data.name; - }); - promise.catch(function (response) { - ProcessErrors($rootScope, response.data, response.status, null, { - hdr: 'Error!', - msg: 'Failed to get inventory name. GET returned status: ' + - response.status }); - $location.path("/inventories/"); - }); - return promise; - }; - // set the arguments help to watch on change of the module privateFn.instantiateArgumentHelp = function() { $scope.$watch('module_name', function(val) { @@ -167,9 +150,6 @@ function adhocController($q, $scope, $rootScope, $location, $routeParams, // put the inventory id on scope for the partial to use $scope.inv_id = id; - // get the inventory name - privateFn.getInventoryNameForBreadcrumbs(urls.inventoryUrl); - // set the arguments help to watch on change of the module privateFn.instantiateArgumentHelp(); @@ -199,7 +179,7 @@ function adhocController($q, $scope, $rootScope, $location, $routeParams, // launch the job with the provided form data $scope.launchJob = function () { - var adhocUrl = GetBasePath('inventory') + $routeParams.inventory_id + + var adhocUrl = GetBasePath('inventory') + $stateParams.inventory_id + '/ad_hoc_commands/', fld, data={}, html; html = '
    - - - -
    diff --git a/awx/ui/client/src/adhoc/main.js b/awx/ui/client/src/adhoc/main.js index e4d8d26bf7..e854ce9f97 100644 --- a/awx/ui/client/src/adhoc/main.js +++ b/awx/ui/client/src/adhoc/main.js @@ -2,11 +2,9 @@ import route from './adhoc.route'; import adhocController from './adhoc.controller'; import form from './adhoc.form'; -export default angular.module('adhoc', ["ngRoute"]) +export default angular.module('adhoc', []) .controller('adhocController', adhocController) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); }]) .factory('adhocForm', form); diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index bd07b7d3e9..3af813dd8c 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -31,9 +31,6 @@ import systemTracking from './system-tracking/main'; import inventoryScripts from './inventory-scripts/main'; import permissions from './permissions/main'; import managementJobs from './management-jobs/main'; -import routeExtensions from './shared/route-extensions/main'; -import breadcrumbs from './shared/breadcrumbs/main'; - // modules import setupMenu from './setup-menu/main'; @@ -78,13 +75,10 @@ __deferLoadIfEnabled(); var tower = angular.module('Tower', [ // 'ngAnimate', - 'ngRoute', 'ngSanitize', 'ngCookies', RestServices.name, - routeExtensions.name, browserData.name, - breadcrumbs.name, systemTracking.name, inventoryScripts.name, permissions.name, @@ -185,7 +179,9 @@ var tower = angular.module('Tower', [ 'PortalJobsListDefinition', 'features', 'longDateFilter', - 'pendolytics' + 'pendolytics', + 'ui.router', + 'ncy-angular-breadcrumb', ]) .constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/') @@ -195,14 +191,74 @@ var tower = angular.module('Tower', [ .config(['$pendolyticsProvider', function($pendolyticsProvider) { $pendolyticsProvider.doNotAutoStart(); }]) - .config(['$routeProvider', - function ($routeProvider) { - $routeProvider. + .config(['$stateProvider', '$urlRouterProvider', '$breadcrumbProvider', + function ($stateProvider, $urlRouterProvider, $breadcrumbProvider) { - when('/jobs', { - name: 'jobs', + $breadcrumbProvider.setOptions({ + templateUrl: urlPrefix + 'partials/breadcrumb.html' + }); + + // $urlRouterProvider.otherwise("/home"); + $urlRouterProvider.otherwise(function($injector){ + var $state = $injector.get("$state"); + $state.go('dashboard'); + }); + + $stateProvider. + state('dashboard', { + url: '/home', + templateUrl: urlPrefix + 'partials/home.html', + controller: Home, + ncyBreadcrumb: { + label: "DASHBOARD" + }, + resolve: { + graphData: ['$q', 'jobStatusGraphData', 'FeaturesService', function($q, jobStatusGraphData, FeaturesService) { + return $q.all({ + jobStatus: jobStatusGraphData.get("month", "all"), + features: FeaturesService.get() + }); + }] + } + }). + + state('dashboardGroups', { + url: '/home/groups', + templateUrl: urlPrefix + 'partials/subhome.html', + controller: HomeGroups, + ncyBreadcrumb: { + parent: 'dashboard', + label: "GROUPS" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } + }). + + state('dashboardHosts', { + url: '/home/hosts?has_active_failures', + templateUrl: urlPrefix + 'partials/subhome.html', + controller: HomeHosts, + ncyBreadcrumb: { + parent: 'dashboard', + label: "HOSTS" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } + }). + + state('jobs', { + url: '/jobs', templateUrl: urlPrefix + 'partials/jobs.html', controller: JobsListController, + ncyBreadcrumb: { + label: "JOBS" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -210,10 +266,13 @@ var tower = angular.module('Tower', [ } }). - when('/portal', { - name: 'portal', + state('portal', { + url: '/portal', templateUrl: urlPrefix + 'partials/portal.html', controller: PortalController, + ncyBreadcrumb: { + label: "PORTAL" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -221,10 +280,14 @@ var tower = angular.module('Tower', [ } }). - when('/jobs/:id', { - name: 'jobDetail', + state('jobDetail', { + url: '/jobs/:id', templateUrl: urlPrefix + 'partials/job_detail.html', controller: JobDetailController, + ncyBreadcrumb: { + parent: 'jobs', + label: "{{ job.id }} - {{ job.name }}" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -244,10 +307,14 @@ var tower = angular.module('Tower', [ } }). - when('/jobs/:id/stdout', { - name: 'jobsStdout', + state('jobsStdout', { + url: '/jobs/:id/stdout', templateUrl: urlPrefix + 'partials/job_stdout.html', controller: JobStdoutController, + ncyBreadcrumb: { + parent: 'jobDetail', + label: "STANDARD OUT" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -267,8 +334,8 @@ var tower = angular.module('Tower', [ } }). - when('/ad_hoc_commands/:id', { - name: 'adHocJobStdout', + state('adHocJobStdout', { + url: '/ad_hoc_commands/:id', templateUrl: urlPrefix + 'partials/job_stdout_adhoc.html', controller: JobStdoutController, resolve: { @@ -290,10 +357,13 @@ var tower = angular.module('Tower', [ } }). - when('/job_templates', { - name: 'jobTemplates', + state('jobTemplates', { + url: '/job_templates', templateUrl: urlPrefix + 'partials/job_templates.html', controller: JobTemplatesList, + ncyBreadcrumb: { + label: "JOB TEMPLATES" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -301,10 +371,14 @@ var tower = angular.module('Tower', [ } }). - when('/job_templates/add', { - name: 'jobTemplateAdd', + state('jobTemplateAdd', { + url: '/job_templates/add', templateUrl: urlPrefix + 'partials/job_templates.html', controller: JobTemplatesAdd, + ncyBreadcrumb: { + parent: "jobTemplates", + label: "CREATE JOB TEMPLATE" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -312,8 +386,8 @@ var tower = angular.module('Tower', [ } }). - when('/job_templates/:template_id', { - name: 'jobTemplateEdit', + state('jobTemplateEdit', { + url: '/job_templates/:template_id', templateUrl: urlPrefix + 'partials/job_templates.html', controller: JobTemplatesEdit, resolve: { @@ -323,8 +397,8 @@ var tower = angular.module('Tower', [ } }). - when('/job_templates/:id/schedules', { - name: 'jobTemplateSchedules', + state('jobTemplateSchedules', { + url: '/job_templates/:id/schedules', templateUrl: urlPrefix + 'partials/schedule_detail.html', controller: ScheduleEditController, resolve: { @@ -334,10 +408,13 @@ var tower = angular.module('Tower', [ } }). - when('/projects', { - name: 'projects', + state('projects', { + url: '/projects', templateUrl: urlPrefix + 'partials/projects.html', controller: ProjectsList, + ncyBreadcrumb: { + label: "PROJECTS" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -345,10 +422,14 @@ var tower = angular.module('Tower', [ } }). - when('/projects/add', { - name: 'projectAdd', - templateUrl: urlPrefix + 'partials/projects.html', + state('projects.add', { + url: '/add', + templateUrl: urlPrefix + 'partials/projects.add.html', controller: ProjectsAdd, + ncyBreadcrumb: { + parent: "projects", + label: "CREATE PROJECT" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -356,8 +437,8 @@ var tower = angular.module('Tower', [ } }). - when('/projects/:id', { - name: 'projectEdit', + state('projects.edit', { + url: '/:id', templateUrl: urlPrefix + 'partials/projects.html', controller: ProjectsEdit, resolve: { @@ -367,8 +448,8 @@ var tower = angular.module('Tower', [ } }). - when('/projects/:id/schedules', { - name: 'projectSchedules', + state('projectSchedules', { + url: '/projects/:id/schedules', templateUrl: urlPrefix + 'partials/schedule_detail.html', controller: ScheduleEditController, resolve: { @@ -378,8 +459,8 @@ var tower = angular.module('Tower', [ } }). - when('/projects/:project_id/organizations', { - name: 'projectOrganizations', + state('projectOrganizations', { + url: '/projects/:project_id/organizations', templateUrl: urlPrefix + 'partials/projects.html', controller: OrganizationsList, resolve: { @@ -389,8 +470,8 @@ var tower = angular.module('Tower', [ } }). - when('/projects/:project_id/organizations/add', { - name: 'projectOrganizationAdd', + state('projectOrganizationAdd', { + url: '/projects/:project_id/organizations/add', templateUrl: urlPrefix + 'partials/projects.html', controller: OrganizationsAdd, resolve: { @@ -400,10 +481,13 @@ var tower = angular.module('Tower', [ } }). - when('/inventories', { - name: 'inventories', + state('inventories', { + url: '/inventories', templateUrl: urlPrefix + 'partials/inventories.html', controller: InventoriesList, + ncyBreadcrumb: { + label: "INVENTORIES" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -411,10 +495,14 @@ var tower = angular.module('Tower', [ } }). - when('/inventories/add', { - name: 'inventoryAdd', + state('inventories.add', { + url: '/add', templateUrl: urlPrefix + 'partials/inventories.html', controller: InventoriesAdd, + ncyBreadcrumb: { + parent: "inventories", + label: "CREATE INVENTORY" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -422,8 +510,8 @@ var tower = angular.module('Tower', [ } }). - when('/inventories/:inventory_id', { - name: 'inventoryEdit', + state('inventories.edit', { + url: '/:inventory_id', templateUrl: urlPrefix + 'partials/inventories.html', controller: InventoriesEdit, resolve: { @@ -433,8 +521,8 @@ var tower = angular.module('Tower', [ } }). - when('/inventories/:inventory_id/job_templates/add', { - name: 'inventoryJobTemplateAdd', + state('inventoryJobTemplateAdd', { + url: '/inventories/:inventory_id/job_templates/add', templateUrl: urlPrefix + 'partials/job_templates.html', controller: JobTemplatesAdd, resolve: { @@ -444,12 +532,8 @@ var tower = angular.module('Tower', [ } }). - when('/inventories/:inventory_id/job_templates/', { - redirectTo: '/inventories/:inventory_id' - }). - - when('/inventories/:inventory_id/job_templates/:template_id', { - name: 'inventoryJobTemplateEdit', + state('inventoryJobTemplateEdit', { + url: '/inventories/:inventory_id/job_templates/:template_id', templateUrl: urlPrefix + 'partials/job_templates.html', controller: JobTemplatesEdit, resolve: { @@ -459,8 +543,8 @@ var tower = angular.module('Tower', [ } }). - when('/inventories/:inventory_id/manage', { - name: 'inventoryManage', + state('inventoryManage', { + url: '/inventories/:inventory_id/manage?groups', templateUrl: urlPrefix + 'partials/inventory-manage.html', controller: InventoriesManage, resolve: { @@ -470,10 +554,14 @@ var tower = angular.module('Tower', [ } }). - when('/organizations', { - name: 'organizations', + state('organizations', { + url: '/organizations', templateUrl: urlPrefix + 'partials/organizations.html', controller: OrganizationsList, + ncyBreadcrumb: { + parent: "setup", + label: "ORGANIZATIONS" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -481,10 +569,14 @@ var tower = angular.module('Tower', [ } }). - when('/organizations/add', { - name: 'organizationAdd', + state('organizationsAdd', { + url: '/organization/add', templateUrl: urlPrefix + 'partials/organizations.html', controller: OrganizationsAdd, + ncyBreadcrumb: { + parent: "organizations", + label: "CREATE ORGANIZATION" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -492,8 +584,8 @@ var tower = angular.module('Tower', [ } }). - when('/organizations/:organization_id', { - name: 'organizationEdit', + state('organizationEdit', { + url: '/organizations/:organization_id', templateUrl: urlPrefix + 'partials/organizations.html', controller: OrganizationsEdit, resolve: { @@ -503,8 +595,8 @@ var tower = angular.module('Tower', [ } }). - when('/organizations/:organization_id/admins', { - name: 'organizationAdmins', + state('organizationAdmins', { + url: '/organizations/:organization_id/admins', templateUrl: urlPrefix + 'partials/organizations.html', controller: AdminsList, resolve: { @@ -514,8 +606,8 @@ var tower = angular.module('Tower', [ } }). - when('/organizations/:organization_id/users', { - name: 'organizationUsers', + state('organizationUsers', { + url:'/organizations/:organization_id/users', templateUrl: urlPrefix + 'partials/users.html', controller: UsersList, resolve: { @@ -525,8 +617,8 @@ var tower = angular.module('Tower', [ } }). - when('/organizations/:organization_id/users/add', { - name: 'organizationUserAdd', + state('organizationUserAdd', { + url: '/organizations/:organization_id/users/add', templateUrl: urlPrefix + 'partials/users.html', controller: UsersAdd, resolve: { @@ -536,8 +628,8 @@ var tower = angular.module('Tower', [ } }). - when('/organizations/:organization_id/users/:user_id', { - name: 'organizationUserEdit', + state('organizationUserEdit', { + url: '/organizations/:organization_id/users/:user_id', templateUrl: urlPrefix + 'partials/users.html', controller: UsersEdit, resolve: { @@ -547,10 +639,14 @@ var tower = angular.module('Tower', [ } }). - when('/teams', { - name: 'teams', + state('teams', { + url: '/teams', templateUrl: urlPrefix + 'partials/teams.html', controller: TeamsList, + ncyBreadcrumb: { + parent: 'setup', + label: 'TEAMS' + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -558,10 +654,14 @@ var tower = angular.module('Tower', [ } }). - when('/teams/add', { - name: 'teamsAdd', + state('teamsAdd', { + url: '/teams/add', templateUrl: urlPrefix + 'partials/teams.html', controller: TeamsAdd, + ncyBreadcrumb: { + parent: "teams", + label: "CREATE TEAM" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -569,8 +669,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id', { - name: 'teamEdit', + state('teamEdit', { + url: '/teams/:team_id', templateUrl: urlPrefix + 'partials/teams.html', controller: TeamsEdit, resolve: { @@ -580,8 +680,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/users', { - name: 'teamUsers', + state('teamUsers', { + url: '/teams/:team_id/users', templateUrl: urlPrefix + 'partials/teams.html', controller: UsersList, resolve: { @@ -591,8 +691,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/users/:user_id', { - name: 'teamUserEdit', + state('teamUserEdit', { + url: '/teams/:team_id/users/:user_id', templateUrl: urlPrefix + 'partials/teams.html', controller: UsersEdit, resolve: { @@ -602,8 +702,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/projects', { - name: 'teamProjects', + state('teamProjects', { + url: '/teams/:team_id/projects', templateUrl: urlPrefix + 'partials/teams.html', controller: ProjectsList, resolve: { @@ -613,8 +713,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/projects/add', { - name: 'teamProjectAdd', + state('teamProjectAdd', { + url: '/teams/:team_id/projects/add', templateUrl: urlPrefix + 'partials/teams.html', controller: ProjectsAdd, resolve: { @@ -624,8 +724,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/projects/:project_id', { - name: 'teamProjectEdit', + state('teamProjectEdit', { + url: '/teams/:team_id/projects/:project_id', templateUrl: urlPrefix + 'partials/teams.html', controller: ProjectsEdit, resolve: { @@ -635,8 +735,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/credentials', { - name: 'teamCredentials', + state('teamCredentials', { + url: '/teams/:team_id/credentials', templateUrl: urlPrefix + 'partials/teams.html', controller: CredentialsList, resolve: { @@ -646,8 +746,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/credentials/add', { - name: 'teamCredentialAdd', + state('teamCredentialAdd', { + url: '/teams/:team_id/credentials/add', templateUrl: urlPrefix + 'partials/teams.html', controller: CredentialsAdd, resolve: { @@ -657,8 +757,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:team_id/credentials/:credential_id', { - name: 'teamCredentialEdit', + state('teamCredentialEdit', { + url: '/teams/:team_id/credentials/:credential_id', templateUrl: urlPrefix + 'partials/teams.html', controller: CredentialsEdit, resolve: { @@ -668,10 +768,14 @@ var tower = angular.module('Tower', [ } }). - when('/credentials', { - name: 'credentials', + state('credentials', { + url: '/credentials', templateUrl: urlPrefix + 'partials/credentials.html', controller: CredentialsList, + ncyBreadcrumb: { + parent: 'setup', + label: 'CREDENTIALS' + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -679,10 +783,14 @@ var tower = angular.module('Tower', [ } }). - when('/credentials/add', { - name: 'credentialAdd', + state('credentialAdd', { + url: '/credentials/add', templateUrl: urlPrefix + 'partials/credentials.html', controller: CredentialsAdd, + ncyBreadcrumb: { + parent: "credentials", + label: "CREATE CREDENTIAL" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -690,8 +798,8 @@ var tower = angular.module('Tower', [ } }). - when('/credentials/:credential_id', { - name: 'credentialEdit', + state('credentialEdit', { + url: '/credentials/:credential_id', templateUrl: urlPrefix + 'partials/credentials.html', controller: CredentialsEdit, resolve: { @@ -701,10 +809,14 @@ var tower = angular.module('Tower', [ } }). - when('/users', { - name: 'users', + state('users', { + url: '/users', templateUrl: urlPrefix + 'partials/users.html', controller: UsersList, + ncyBreadcrumb: { + parent: 'setup', + label: 'USERS' + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -712,10 +824,14 @@ var tower = angular.module('Tower', [ } }). - when('/users/add', { - name: 'userAdd', + state('userAdd', { + url: '/users/add', templateUrl: urlPrefix + 'partials/users.html', controller: UsersAdd, + ncyBreadcrumb: { + parent: "users", + label: "CREATE USER" + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -723,8 +839,8 @@ var tower = angular.module('Tower', [ } }). - when('/users/:user_id', { - name: 'userEdit', + state('userEdit', { + url: '/users/:user_id', templateUrl: urlPrefix + 'partials/users.html', controller: UsersEdit, resolve: { @@ -734,8 +850,8 @@ var tower = angular.module('Tower', [ } }). - when('/users/:user_id/credentials', { - name: 'userCredentials', + state('userCredentials', { + url: '/users/:user_id/credentials', templateUrl: urlPrefix + 'partials/users.html', controller: CredentialsList, resolve: { @@ -745,8 +861,8 @@ var tower = angular.module('Tower', [ } }). - when('/users/:user_id/credentials/add', { - name: 'userCredentialAdd', + state('userCredentialAdd', { + url: '/users/:user_id/credentials/add', templateUrl: urlPrefix + 'partials/teams.html', controller: CredentialsAdd, resolve: { @@ -756,8 +872,8 @@ var tower = angular.module('Tower', [ } }). - when('/teams/:user_id/credentials/:credential_id', { - name: 'teamUserCredentialEdit', + state('teamUserCredentialEdit', { + url: '/teams/:user_id/credentials/:credential_id', templateUrl: urlPrefix + 'partials/teams.html', controller: CredentialsEdit, resolve: { @@ -767,46 +883,14 @@ var tower = angular.module('Tower', [ } }). - when('/home', { - name: 'dashboard', - templateUrl: urlPrefix + 'partials/home.html', - controller: Home, - resolve: { - graphData: ['$q', 'jobStatusGraphData', 'FeaturesService', function($q, jobStatusGraphData, FeaturesService) { - return $q.all({ - jobStatus: jobStatusGraphData.get("month", "all"), - features: FeaturesService.get() - }); - }] - } - }). - - when('/home/groups', { - name: 'dashboardGroups', - templateUrl: urlPrefix + 'partials/subhome.html', - controller: HomeGroups, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - - when('/home/hosts', { - name: 'dashboardHosts', - templateUrl: urlPrefix + 'partials/subhome.html', - controller: HomeHosts, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - - when('/license', { - name: 'license', + state('license', { + url: '/license', templateUrl: urlPrefix + 'partials/license.html', controller: LicenseController, + ncyBreadcrumb: { + parent: 'setup', + label: 'LICENSE' + }, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); @@ -814,14 +898,13 @@ var tower = angular.module('Tower', [ } }). - when('/sockets', { - name: 'sockets', + state('sockets', { + url: '/sockets', templateUrl: urlPrefix + 'partials/sockets.html', - controller: SocketsController - }). - - otherwise({ - redirectTo: '/home' + controller: SocketsController, + ncyBreadcrumb: { + label: 'SOCKETS' + } }); } ]) @@ -885,7 +968,6 @@ var tower = angular.module('Tower', [ $rootScope.removeConfigReady = $rootScope.$on('ConfigReady', function() { LoadBasePaths(); - $rootScope.breadcrumbs = []; $rootScope.crumbCache = []; if ($rootScope.removeOpenSocket) { @@ -966,7 +1048,7 @@ var tower = angular.module('Tower', [ },2000); }); - $rootScope.$on("$routeChangeStart", function (event, next, prev) { + $rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) { // this line removes the query params attached to a route if(prev && prev.$$route && prev.$$route.name === 'systemTracking'){ @@ -1027,7 +1109,7 @@ var tower = angular.module('Tower', [ } else { // If browser refresh, set the user_is_superuser value $rootScope.user_is_superuser = Authorization.getUserInfo('is_superuser'); - // when the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket) + // state the user refreshes we want to open the socket, except if the user is on the login page, which should happen after the user logs in (see the AuthService module for that call to OpenSocket) if(!_.contains($location.$$url, '/login')){ Timer.init().then(function(timer){ $rootScope.sessionTimer = timer; @@ -1068,7 +1150,7 @@ var tower = angular.module('Tower', [ if (!$AnsibleConfig) { - // create a promise that will resolve when $AnsibleConfig is loaded + // create a promise that will resolve state $AnsibleConfig is loaded $rootScope.loginConfig = $q.defer(); } diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.block.less b/awx/ui/client/src/bread-crumb/bread-crumb.block.less index af6f4f57de..99edea97d5 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.block.less +++ b/awx/ui/client/src/bread-crumb/bread-crumb.block.less @@ -39,6 +39,29 @@ flex: initial; } +.BreadCrumb > ol { + padding: 0px 20px; + list-style: none; + background-color: #FFFFFF; + border-radius: 4px; + line-height: 40px; +} + +.BreadCrumb > ol > li { + display: inline-block; + color: #B7B7B7; +} + +.BreadCrumb > ol > li + li:before { + content: "/"; + padding: 0 5px; + color: #B7B7B7; +} + +.BreadCrumb > ol > li > .active { + color: #123123; +} + @breadcrumb-breakpoint: 900px; @media screen and (max-width: @breadcrumb-breakpoint) { diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index 55e3db0a47..2ffc037268 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -1,7 +1,7 @@ /* jshint unused: vars */ export default - [ 'templateUrl', '$route', function(templateUrl, $route) { + [ 'templateUrl', '$state', function(templateUrl, $state) { return { restrict: 'E', templateUrl: templateUrl('bread-crumb/bread-crumb'), @@ -10,11 +10,11 @@ export default scope.toggleActivityStreamActive = function(){ scope.activityStreamActive = !scope.activityStreamActive; - } + }; scope.isActive = function (path) { - if ($route.current && $route.current.regexp) { - return $route.current.regexp.test(path); + if ($state.current && $state.current.regexp) { + return $state.current.regexp.test(path); } return false; }; diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html index f49d02393a..7a91909d1c 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html +++ b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html @@ -1,4 +1,5 @@ "; diff --git a/awx/ui/client/src/helpers/Schedules.js b/awx/ui/client/src/helpers/Schedules.js index 5d45098833..0537a07c4d 100644 --- a/awx/ui/client/src/helpers/Schedules.js +++ b/awx/ui/client/src/helpers/Schedules.js @@ -236,9 +236,9 @@ export default }; }]) - .factory('AddSchedule', ['$location', '$routeParams', 'SchedulerInit', 'ShowSchedulerModal', 'Wait', 'GetBasePath', 'Empty', + .factory('AddSchedule', ['$location', '$stateParams', 'SchedulerInit', 'ShowSchedulerModal', 'Wait', 'GetBasePath', 'Empty', 'SchedulePost', - function($location, $routeParams, SchedulerInit, ShowSchedulerModal, Wait, GetBasePath, Empty, SchedulePost) { + function($location, $stateParams, SchedulerInit, ShowSchedulerModal, Wait, GetBasePath, Empty, SchedulePost) { return function(params) { var scope = params.scope, callback= params.callback, @@ -246,14 +246,14 @@ export default url = GetBasePath(base), scheduler; - if (!Empty($routeParams.template_id)) { - url += $routeParams.template_id + '/schedules/'; + if (!Empty($stateParams.template_id)) { + url += $stateParams.template_id + '/schedules/'; } - else if (!Empty($routeParams.id)) { - url += $routeParams.id + '/schedules/'; + else if (!Empty($stateParams.id)) { + url += $stateParams.id + '/schedules/'; } - else if (!Empty($routeParams.management_job)) { - url += $routeParams.management_job + '/schedules/'; + else if (!Empty($stateParams.management_job)) { + url += $stateParams.management_job + '/schedules/'; if(scope.management_job.id === 4){ scope.isFactCleanup = true; scope.keep_unit_choices = [{ @@ -690,9 +690,9 @@ export default * Called from a controller to setup the scope for a schedules list * */ - .factory('LoadSchedulesScope', ['$compile', '$location', '$routeParams','SearchInit', 'PaginateInit', 'generateList', 'SchedulesControllerInit', + .factory('LoadSchedulesScope', ['$compile', '$location', '$stateParams','SearchInit', 'PaginateInit', 'generateList', 'SchedulesControllerInit', 'SchedulesListInit', - function($compile, $location, $routeParams, SearchInit, PaginateInit, GenerateList, SchedulesControllerInit, SchedulesListInit) { + function($compile, $location, $stateParams, SearchInit, PaginateInit, GenerateList, SchedulesControllerInit, SchedulesListInit) { return function(params) { var parent_scope = params.parent_scope, scope = params.scope, @@ -707,7 +707,6 @@ export default GenerateList.inject(list, { mode: 'edit', id: id, - breadCrumbs: false, scope: scope, searchSize: (searchSize) ? searchSize : 'col-lg-6 col-md-6 col-sm-6 col-xs-12', showSearch: true @@ -746,9 +745,9 @@ export default parent_scope.$emit('listLoaded'); }); - if ($routeParams.id__int) { + if ($stateParams.id__int) { scope[list.iterator + 'SearchField'] = 'id'; - scope[list.iterator + 'SearchValue'] = $routeParams.id__int; + scope[list.iterator + 'SearchValue'] = $stateParams.id__int; scope[list.iterator + 'SearchFieldLabel'] = 'ID'; } diff --git a/awx/ui/client/src/helpers/inventory.js b/awx/ui/client/src/helpers/inventory.js index 87a80f27e7..cd85eb2d04 100644 --- a/awx/ui/client/src/helpers/inventory.js +++ b/awx/ui/client/src/helpers/inventory.js @@ -106,7 +106,6 @@ export default showButtons: false, showActions: false, id: 'inventory-edit-modal-dialog', - breadCrumbs: false, related: false, scope: scope }); diff --git a/awx/ui/client/src/inventory-scripts/add/add.controller.js b/awx/ui/client/src/inventory-scripts/add/add.controller.js index 29f431726a..33aada3daa 100644 --- a/awx/ui/client/src/inventory-scripts/add/add.controller.js +++ b/awx/ui/client/src/inventory-scripts/add/add.controller.js @@ -8,12 +8,12 @@ export default [ '$compile','SchedulerInit', 'Rest', 'Wait', 'inventoryScriptsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty', 'GenerateForm', 'SearchInit' , 'PaginateInit', - 'LookUpInit', 'OrganizationList', '$scope', 'transitionTo', + 'LookUpInit', 'OrganizationList', '$scope', '$state', function( $compile, SchedulerInit, Rest, Wait, inventoryScriptsFormObject, ProcessErrors, GetBasePath, Empty, GenerateForm, SearchInit, PaginateInit, - LookUpInit, OrganizationList, $scope, transitionTo + LookUpInit, OrganizationList, $scope, $state ) { var scope = $scope, generator = GenerateForm, @@ -48,7 +48,7 @@ export default script: scope.script }) .success(function () { - transitionTo('inventoryScriptsList'); + $state.transitionTo('inventoryScriptsList'); Wait('stop'); }) diff --git a/awx/ui/client/src/inventory-scripts/add/add.partial.html b/awx/ui/client/src/inventory-scripts/add/add.partial.html index a9e7a04b9b..aa722e6705 100644 --- a/awx/ui/client/src/inventory-scripts/add/add.partial.html +++ b/awx/ui/client/src/inventory-scripts/add/add.partial.html @@ -1,12 +1,3 @@ - - - - - - -
    diff --git a/awx/ui/client/src/inventory-scripts/add/add.route.js b/awx/ui/client/src/inventory-scripts/add/add.route.js index c9a7703fcb..0cd9b020b9 100644 --- a/awx/ui/client/src/inventory-scripts/add/add.route.js +++ b/awx/ui/client/src/inventory-scripts/add/add.route.js @@ -15,5 +15,9 @@ export default { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }] + }, + ncyBreadcrumb: { + parent: 'inventoryScriptsList', + label: 'CREATE INVENTORY SCRIPT' } }; diff --git a/awx/ui/client/src/inventory-scripts/add/main.js b/awx/ui/client/src/inventory-scripts/add/main.js index 3e5090566d..f96a334435 100644 --- a/awx/ui/client/src/inventory-scripts/add/main.js +++ b/awx/ui/client/src/inventory-scripts/add/main.js @@ -10,8 +10,6 @@ import controller from './add.controller'; export default angular.module('inventoryScriptsAdd', []) .controller('inventoryScriptsAddController', controller) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); }]); diff --git a/awx/ui/client/src/inventory-scripts/edit/edit.controller.js b/awx/ui/client/src/inventory-scripts/edit/edit.controller.js index 0d2d2c52ed..a464338cd4 100644 --- a/awx/ui/client/src/inventory-scripts/edit/edit.controller.js +++ b/awx/ui/client/src/inventory-scripts/edit/edit.controller.js @@ -9,13 +9,13 @@ export default 'inventoryScriptsFormObject', 'ProcessErrors', 'GetBasePath', 'GenerateForm', 'SearchInit' , 'PaginateInit', 'LookUpInit', 'OrganizationList', 'inventory_script', - '$scope', 'transitionTo', + '$scope', '$state', function( Rest, Wait, inventoryScriptsFormObject, ProcessErrors, GetBasePath, GenerateForm, SearchInit, PaginateInit, LookUpInit, OrganizationList, inventory_script, - $scope, transitionTo + $scope, $state ) { var generator = GenerateForm, id = inventory_script.id, @@ -27,7 +27,6 @@ export default generator.inject(form, { mode: 'edit' , scope:$scope, - breadCrumbs: true, related: false, activityStream: false }); @@ -80,7 +79,7 @@ export default script: $scope.script }) .success(function () { - transitionTo('inventoryScriptsList'); + $state.transitionTo('inventoryScriptsList'); Wait('stop'); }) diff --git a/awx/ui/client/src/inventory-scripts/edit/edit.partial.html b/awx/ui/client/src/inventory-scripts/edit/edit.partial.html index 854bcf4fad..555ab2462d 100644 --- a/awx/ui/client/src/inventory-scripts/edit/edit.partial.html +++ b/awx/ui/client/src/inventory-scripts/edit/edit.partial.html @@ -1,9 +1,3 @@ - - - - - -
    diff --git a/awx/ui/client/src/inventory-scripts/edit/edit.route.js b/awx/ui/client/src/inventory-scripts/edit/edit.route.js index 2f0d9cf2e3..9d419045b3 100644 --- a/awx/ui/client/src/inventory-scripts/edit/edit.route.js +++ b/awx/ui/client/src/inventory-scripts/edit/edit.route.js @@ -8,25 +8,27 @@ import {templateUrl} from '../../shared/template-url/template-url.factory'; export default { name: 'inventoryScriptsEdit', - route: '/inventory_scripts/:inventory_script', + route: '/inventory_scripts/:inventory_script_id', templateUrl: templateUrl('inventory-scripts/edit/edit'), controller: 'inventoryScriptsEditController', + params: {inventory_script: null}, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }], inventory_script: - [ '$route', + [ '$state', + '$stateParams', '$q', 'Rest', 'GetBasePath', 'ProcessErrors', - function($route, $q, rest, getBasePath, ProcessErrors) { - if ($route.current.hasModelKey('inventory_script')) { - return $q.when($route.current.params.model.inventory_script); + function($state, $stateParams, $q, rest, getBasePath, ProcessErrors) { + if ($stateParams.inventory_script) { + return $q.when($stateParams.inventory_script); } - var inventoryScriptId = $route.current.params.inventory_script; + var inventoryScriptId = $stateParams.inventory_script_id; var url = getBasePath('inventory_scripts') + inventoryScriptId + '/'; rest.setUrl(url); diff --git a/awx/ui/client/src/inventory-scripts/edit/main.js b/awx/ui/client/src/inventory-scripts/edit/main.js index 17c4980986..beb7bf0797 100644 --- a/awx/ui/client/src/inventory-scripts/edit/main.js +++ b/awx/ui/client/src/inventory-scripts/edit/main.js @@ -10,8 +10,6 @@ import controller from './edit.controller'; export default angular.module('inventoryScriptsEdit', []) .controller('inventoryScriptsEditController', controller) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); }]); diff --git a/awx/ui/client/src/inventory-scripts/list/list.controller.js b/awx/ui/client/src/inventory-scripts/list/list.controller.js index c84d824b46..9d8bb07289 100644 --- a/awx/ui/client/src/inventory-scripts/list/list.controller.js +++ b/awx/ui/client/src/inventory-scripts/list/list.controller.js @@ -7,11 +7,11 @@ export default [ '$rootScope','Wait', 'generateList', 'inventoryScriptsListObject', 'GetBasePath' , 'SearchInit' , 'PaginateInit', - 'Rest' , 'ProcessErrors', 'Prompt', 'transitionTo', 'Stream', + 'Rest' , 'ProcessErrors', 'Prompt', '$state', 'Stream', function( $rootScope,Wait, GenerateList, inventoryScriptsListObject, GetBasePath, SearchInit, PaginateInit, - Rest, ProcessErrors, Prompt, transitionTo, Stream + Rest, ProcessErrors, Prompt, $state, Stream ) { var scope = $rootScope.$new(), defaultUrl = GetBasePath('inventory_scripts'), @@ -38,7 +38,8 @@ export default scope.search(list.iterator); scope.editCustomInv = function(){ - transitionTo('inventoryScriptsEdit', { + $state.transitionTo('inventoryScriptsEdit',{ + inventory_script_id: this.inventory_script.id, inventory_script: this.inventory_script }); }; @@ -73,7 +74,7 @@ export default }; scope.addCustomInv = function(){ - transitionTo('inventoryScriptsAdd'); + $state.transitionTo('inventoryScriptsAdd'); }; } diff --git a/awx/ui/client/src/inventory-scripts/list/list.partial.html b/awx/ui/client/src/inventory-scripts/list/list.partial.html index 6adbbb27e6..33085edcb3 100644 --- a/awx/ui/client/src/inventory-scripts/list/list.partial.html +++ b/awx/ui/client/src/inventory-scripts/list/list.partial.html @@ -1,8 +1,3 @@ - - - - -
    diff --git a/awx/ui/client/src/inventory-scripts/list/list.route.js b/awx/ui/client/src/inventory-scripts/list/list.route.js index 21b9197fb4..0a5423f6e3 100644 --- a/awx/ui/client/src/inventory-scripts/list/list.route.js +++ b/awx/ui/client/src/inventory-scripts/list/list.route.js @@ -15,5 +15,9 @@ export default { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }] + }, + ncyBreadcrumb: { + parent: 'setup', + label: 'INVENTORY SCRIPTS' } }; diff --git a/awx/ui/client/src/inventory-scripts/list/main.js b/awx/ui/client/src/inventory-scripts/list/main.js index b1f23354cb..60c4a84d33 100644 --- a/awx/ui/client/src/inventory-scripts/list/main.js +++ b/awx/ui/client/src/inventory-scripts/list/main.js @@ -10,8 +10,6 @@ import controller from './list.controller'; export default angular.module('inventoryScriptsList', []) .controller('inventoryScriptsListController', controller) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); - }]); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/job-templates/survey-maker/questions/edit.factory.js b/awx/ui/client/src/job-templates/survey-maker/questions/edit.factory.js index 6a4ab46eea..2b6ceb420c 100644 --- a/awx/ui/client/src/job-templates/survey-maker/questions/edit.factory.js +++ b/awx/ui/client/src/job-templates/survey-maker/questions/edit.factory.js @@ -77,7 +77,7 @@ export default } scope.removeGenerateForm = scope.$on('GenerateForm', function() { tmpVar = scope.mode; - GenerateForm.inject(form, { id: 'question_'+index, mode: 'edit' , related: false, scope:scope, breadCrumbs: false}); + GenerateForm.inject(form, { id: 'question_'+index, mode: 'edit' , related: false, scope:scope }); scope.mode = tmpVar; scope.$emit('FillQuestionForm'); }); diff --git a/awx/ui/client/src/job-templates/survey-maker/shared/question-definition.form.js b/awx/ui/client/src/job-templates/survey-maker/shared/question-definition.form.js index 01bfff1f5f..fa416b1768 100644 --- a/awx/ui/client/src/job-templates/survey-maker/shared/question-definition.form.js +++ b/awx/ui/client/src/job-templates/survey-maker/shared/question-definition.form.js @@ -19,7 +19,6 @@ export default name: 'survey_question', well: true, twoColumns: true, - breadcrumbs: false, fields: { question_name: { diff --git a/awx/ui/client/src/job-templates/survey-maker/surveys/add.factory.js b/awx/ui/client/src/job-templates/survey-maker/surveys/add.factory.js index 62b8576d06..0bb42dd274 100644 --- a/awx/ui/client/src/job-templates/survey-maker/surveys/add.factory.js +++ b/awx/ui/client/src/job-templates/survey-maker/surveys/add.factory.js @@ -1,5 +1,5 @@ export default - function AddFactory($location, $routeParams, ShowSurveyModal, Wait) { + function AddFactory($location, $stateParams, ShowSurveyModal, Wait) { return function(params) { var scope = params.scope; @@ -19,7 +19,7 @@ export default AddFactory.$inject = [ '$location', - '$routeParams', + '$stateParams', 'showSurvey', 'Wait' ]; diff --git a/awx/ui/client/src/job-templates/survey-maker/surveys/edit.factory.js b/awx/ui/client/src/job-templates/survey-maker/surveys/edit.factory.js index 857136096f..b303a2f470 100644 --- a/awx/ui/client/src/job-templates/survey-maker/surveys/edit.factory.js +++ b/awx/ui/client/src/job-templates/survey-maker/surveys/edit.factory.js @@ -1,5 +1,5 @@ export default - function EditFactory($routeParams, SchedulerInit, ShowSurveyModal, Wait, Rest, ProcessErrors, GetBasePath, GenerateForm, + function EditFactory($stateParams, SchedulerInit, ShowSurveyModal, Wait, Rest, ProcessErrors, GetBasePath, GenerateForm, Empty, AddSurvey) { return function(params) { var scope = params.scope, @@ -66,7 +66,7 @@ export default EditFactory.$inject = - [ '$routeParams', + [ '$stateParams', 'SchedulerInit', 'showSurvey', 'Wait', diff --git a/awx/ui/client/src/job-templates/survey-maker/surveys/init.factory.js b/awx/ui/client/src/job-templates/survey-maker/surveys/init.factory.js index 31e584b946..2495daf614 100644 --- a/awx/ui/client/src/job-templates/survey-maker/surveys/init.factory.js +++ b/awx/ui/client/src/job-templates/survey-maker/surveys/init.factory.js @@ -63,7 +63,7 @@ export default scope.addQuestion = function(){ var tmpMode = scope.mode; - GenerateForm.inject(form, { id:'new_question', mode: 'add' , scope: scope, related: false, breadCrumbs: false}); + GenerateForm.inject(form, { id:'new_question', mode: 'add' , scope: scope, related: false}); scope.mode = tmpMode; scope.required = true; //set the required checkbox to true via the ngmodel attached to scope.required. scope.text_min = null; diff --git a/awx/ui/client/src/login/authenticationServices/timer.factory.js b/awx/ui/client/src/login/authenticationServices/timer.factory.js index 7bbc3356c4..dce6520023 100644 --- a/awx/ui/client/src/login/authenticationServices/timer.factory.js +++ b/awx/ui/client/src/login/authenticationServices/timer.factory.js @@ -22,9 +22,9 @@ * @description */ export default - ['$rootScope', '$cookieStore', 'transitionTo', 'CreateDialog', 'Authorization', + ['$rootScope', '$cookieStore', 'CreateDialog', 'Authorization', 'Store', '$interval', '$location', '$q', - function ($rootScope, $cookieStore, transitionTo, CreateDialog, Authorization, + function ($rootScope, $cookieStore, CreateDialog, Authorization, Store, $interval, $location, $q) { return { diff --git a/awx/ui/client/src/login/login.route.js b/awx/ui/client/src/login/login.route.js index cee67eebfe..eb38c392df 100644 --- a/awx/ui/client/src/login/login.route.js +++ b/awx/ui/client/src/login/login.route.js @@ -15,5 +15,9 @@ export default { Authorization.logout(); } $(".LoginModal-dialog").remove(); - }] + }], + ncyBreadcrumb: { + skip: true + } + }; diff --git a/awx/ui/client/src/login/logout.route.js b/awx/ui/client/src/login/logout.route.js index 86f4be8f29..00e13f3155 100644 --- a/awx/ui/client/src/login/logout.route.js +++ b/awx/ui/client/src/login/logout.route.js @@ -13,5 +13,8 @@ export default { Authorization.logout(); $location.path('/login'); }], + ncyBreadcrumb: { + skip: true + }, templateUrl: '/static/partials/blank.html' }; diff --git a/awx/ui/client/src/login/main.js b/awx/ui/client/src/login/main.js index e4326a7d89..4835feebd3 100644 --- a/awx/ui/client/src/login/main.js +++ b/awx/ui/client/src/login/main.js @@ -12,11 +12,7 @@ import logoutRoute from './logout.route'; export default angular.module('login', [authentication.name, loginModal.name]) - .config(['$routeProvider', function($routeProvider) { - var url = loginRoute.route; - delete loginRoute.route; - $routeProvider.when(url, loginRoute); - url = logoutRoute.route; - delete logoutRoute.route; - $routeProvider.when(url, logoutRoute); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(loginRoute); + $stateExtender.addState(logoutRoute); }]); diff --git a/awx/ui/client/src/management-jobs/list/list.controller.js b/awx/ui/client/src/management-jobs/list/list.controller.js index 98c255adea..fbc098f0b1 100644 --- a/awx/ui/client/src/management-jobs/list/list.controller.js +++ b/awx/ui/client/src/management-jobs/list/list.controller.js @@ -11,12 +11,12 @@ export default 'GetBasePath' , 'SearchInit' , 'PaginateInit', 'SchedulesList', 'Rest' , 'ProcessErrors', 'managementJobsListObject', '$rootScope', - 'transitionTo', 'Stream', + '$state', 'Stream', function( Wait, $location, $compile, CreateDialog, GenerateList, GetBasePath, SearchInit, PaginateInit, SchedulesList, Rest, ProcessErrors, managementJobsListObject, $rootScope, - transitionTo, Stream) { + $state, Stream) { var scope = $rootScope.$new(), parent_scope = scope, @@ -251,8 +251,9 @@ export default }; scope.configureSchedule = function() { - transitionTo('managementJobsSchedule', { - management_job: this.configure_job + $state.transitionTo('managementJobsSchedule', { + management_job: this.configure_job, + management_job_id: this.configure_job.id }); }; diff --git a/awx/ui/client/src/management-jobs/list/list.partial.html b/awx/ui/client/src/management-jobs/list/list.partial.html index 01e12151f9..7e8fb31e45 100644 --- a/awx/ui/client/src/management-jobs/list/list.partial.html +++ b/awx/ui/client/src/management-jobs/list/list.partial.html @@ -1,8 +1,3 @@ - - - - -
    diff --git a/awx/ui/client/src/management-jobs/list/list.route.js b/awx/ui/client/src/management-jobs/list/list.route.js index 8f8dd079c5..0c2d100803 100644 --- a/awx/ui/client/src/management-jobs/list/list.route.js +++ b/awx/ui/client/src/management-jobs/list/list.route.js @@ -15,5 +15,9 @@ export default { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }] - } + }, + ncyBreadcrumb: { + parent: 'setup', + label: 'MANAGEMENT JOBS' + }, }; diff --git a/awx/ui/client/src/management-jobs/list/main.js b/awx/ui/client/src/management-jobs/list/main.js index e0931ff394..85c1367893 100644 --- a/awx/ui/client/src/management-jobs/list/main.js +++ b/awx/ui/client/src/management-jobs/list/main.js @@ -10,8 +10,6 @@ import controller from './list.controller'; export default angular.module('managementJobsList', []) .controller('managementJobsListController', controller) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); }]); diff --git a/awx/ui/client/src/management-jobs/schedule/main.js b/awx/ui/client/src/management-jobs/schedule/main.js index ed46f0090b..e71036c9d6 100644 --- a/awx/ui/client/src/management-jobs/schedule/main.js +++ b/awx/ui/client/src/management-jobs/schedule/main.js @@ -10,8 +10,6 @@ import controller from './schedule.controller'; export default angular.module('managementJobsSchedule', []) .controller('managementJobsScheduleController', controller) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); }]); diff --git a/awx/ui/client/src/management-jobs/schedule/schedule.controller.js b/awx/ui/client/src/management-jobs/schedule/schedule.controller.js index 8abdc80bf6..a25c997e7e 100644 --- a/awx/ui/client/src/management-jobs/schedule/schedule.controller.js +++ b/awx/ui/client/src/management-jobs/schedule/schedule.controller.js @@ -11,10 +11,10 @@ */ export default [ - '$scope', '$location', '$routeParams', 'SchedulesList', 'Rest', + '$scope', '$location', '$stateParams', 'SchedulesList', 'Rest', 'ProcessErrors', 'GetBasePath', 'Wait','LoadSchedulesScope', 'GetChoices', 'Stream', 'management_job', '$rootScope', - function($scope, $location, $routeParams, SchedulesList, Rest, + function($scope, $location, $stateParams, SchedulesList, Rest, ProcessErrors, GetBasePath, Wait, LoadSchedulesScope, GetChoices, Stream, management_job, $rootScope) { var base, id, url, parentObject; @@ -58,7 +58,7 @@ export default [ } $scope.removeChoicesReady = $scope.$on('choicesReady', function() { // Load the parent object - id = $routeParams.management_job; + id = $stateParams.management_job_id; url = GetBasePath('system_job_templates') + id + '/'; Rest.setUrl(url); Rest.get() diff --git a/awx/ui/client/src/management-jobs/schedule/schedule.partial.html b/awx/ui/client/src/management-jobs/schedule/schedule.partial.html index 2374f050c5..43b01efc55 100644 --- a/awx/ui/client/src/management-jobs/schedule/schedule.partial.html +++ b/awx/ui/client/src/management-jobs/schedule/schedule.partial.html @@ -1,9 +1,3 @@ - - - - - -
    diff --git a/awx/ui/client/src/management-jobs/schedule/schedule.route.js b/awx/ui/client/src/management-jobs/schedule/schedule.route.js index 526e7eddd2..060825b075 100644 --- a/awx/ui/client/src/management-jobs/schedule/schedule.route.js +++ b/awx/ui/client/src/management-jobs/schedule/schedule.route.js @@ -8,25 +8,26 @@ import {templateUrl} from '../../shared/template-url/template-url.factory'; export default { name: 'managementJobsSchedule', - route: '/management_jobs/:management_job/schedules', + route: '/management_jobs/:management_job_id/schedules', templateUrl: templateUrl('management-jobs/schedule/schedule'), controller: 'managementJobsScheduleController', + params: {management_job: null}, resolve: { features: ['FeaturesService', function(FeaturesService) { return FeaturesService.get(); }], management_job: - [ '$route', + [ '$stateParams', '$q', 'Rest', 'GetBasePath', 'ProcessErrors', - function($route, $q, rest, getBasePath, ProcessErrors) { - if ($route.current.hasModelKey('management_job')) { - return $q.when($route.current.params.model.management_job); + function($stateParams, $q, rest, getBasePath, ProcessErrors) { + if ($stateParams.management_job) { + return $q.when($stateParams.management_job); } - var managementJobId = $route.current.params.management_job; + var managementJobId = $stateParams.management_job_id; var url = getBasePath('system_job_templates') + managementJobId + '/'; rest.setUrl(url); diff --git a/awx/ui/client/src/partials/breadcrumb.html b/awx/ui/client/src/partials/breadcrumb.html new file mode 100644 index 0000000000..ed0da680b0 --- /dev/null +++ b/awx/ui/client/src/partials/breadcrumb.html @@ -0,0 +1,8 @@ +
      +
    1. + +
    2. +
    3. + +
    4. +
    diff --git a/awx/ui/client/src/partials/credentials.html b/awx/ui/client/src/partials/credentials.html index 089d6cf399..6255311d45 100644 --- a/awx/ui/client/src/partials/credentials.html +++ b/awx/ui/client/src/partials/credentials.html @@ -1,18 +1,3 @@ - - - - - - - - -
    diff --git a/awx/ui/client/src/partials/inventories.html b/awx/ui/client/src/partials/inventories.html index a3b4306a19..10c2f9a7a3 100644 --- a/awx/ui/client/src/partials/inventories.html +++ b/awx/ui/client/src/partials/inventories.html @@ -1,4 +1,5 @@
    +
    diff --git a/awx/ui/client/src/partials/inventory-add.html b/awx/ui/client/src/partials/inventory-add.html new file mode 100644 index 0000000000..a0eaa60be7 --- /dev/null +++ b/awx/ui/client/src/partials/inventory-add.html @@ -0,0 +1,14 @@ +
    + +
    +
    +
    +
    + +
    diff --git a/awx/ui/client/src/partials/inventory-manage.html b/awx/ui/client/src/partials/inventory-manage.html index 0ec23f3107..b8fc629267 100644 --- a/awx/ui/client/src/partials/inventory-manage.html +++ b/awx/ui/client/src/partials/inventory-manage.html @@ -1,12 +1,6 @@
    - -
    diff --git a/awx/ui/client/src/partials/job_detail.html b/awx/ui/client/src/partials/job_detail.html index e1d969f612..e4aba4ec3b 100644 --- a/awx/ui/client/src/partials/job_detail.html +++ b/awx/ui/client/src/partials/job_detail.html @@ -1,15 +1,6 @@
    - -
    diff --git a/awx/ui/client/src/partials/job_stdout.html b/awx/ui/client/src/partials/job_stdout.html index 7175cc8608..6412480fcb 100644 --- a/awx/ui/client/src/partials/job_stdout.html +++ b/awx/ui/client/src/partials/job_stdout.html @@ -5,14 +5,7 @@
    - -
    +
    diff --git a/awx/ui/client/src/partials/job_stdout_adhoc.html b/awx/ui/client/src/partials/job_stdout_adhoc.html index f4e48648f8..78637571e9 100644 --- a/awx/ui/client/src/partials/job_stdout_adhoc.html +++ b/awx/ui/client/src/partials/job_stdout_adhoc.html @@ -3,19 +3,8 @@
    -
    + class="list-actions pull-right col-md-12">
    - +
    @@ -26,7 +26,7 @@
    - + - + @@ -63,7 +63,7 @@ Manage the cleanup of old job history, activity streams, data marked for deletion, and system tracking info.
    - + @@ -73,7 +73,7 @@ Create and edit scripts to dynamically load hosts from any source.
    - +

    View Your License

    \n"; diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 357cd73b09..8996a1bca1 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -9,12 +9,15 @@ import title from './title.directive'; import lodashAsPromised from './lodash-as-promised'; import stringFilters from './string-filters/main'; import truncatedText from './truncated-text.directive'; +import stateExtender from './stateExtender.provider'; export default angular.module('shared', [ listGenerator.name, - stringFilters.name + stringFilters.name, + 'ui.router' ]) .factory('lodashAsPromised', lodashAsPromised) .directive('truncatedText', truncatedText) - .directive('title', title); + .directive('title', title) + .provider('$stateExtender', stateExtender); diff --git a/awx/ui/client/src/shared/route-extensions/link-to.directive.js b/awx/ui/client/src/shared/route-extensions/link-to.directive.js deleted file mode 100644 index a27782a6bc..0000000000 --- a/awx/ui/client/src/shared/route-extensions/link-to.directive.js +++ /dev/null @@ -1,96 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/* jshint unused: vars */ - -import {lookupRouteUrl} from './lookup-route-url'; - -/** - * - * @ngdoc directive - * @name routeExtensions.directive:linkTo - * @desription - * The `linkTo` directive looks up a route's URL and generates a link to that route. When a user - * clicks the link, this directive calls the `transitionTo` factory to send them to the given - * URL. For accessibility and fallback purposes, it also sets the `href` attribute of the link - * to the path. - * - * Note that in this example the model object uses a key that matches up with the route parameteer - * name in the route url (in this case `:id`). - * - * **N.B.** The below example currently won't run. It's included to show an example of using - * the `linkTo` directive within code. In order for this to run, we will need to run - * the code in an iframe (using something like `dgeni` instead of `grunt-ngdocs`). - * - * @example - * - - - angular.module('simpleRouteExample', ['ngRoute', 'routeExtensions']) - .config(['$routeProvider', function($route) { - $route.when('/posts/:id', { - name: 'post', - template: '

    {{post.title}}

    {{post.body}}

    ', - controller: 'post' - }); - }]).controller('post', function($scope) { - }); -
    - -
    - - {{featuredPost.title}} - -
    -
    -
    - * - */ -export default - [ '$route', - '$location', - 'transitionTo', - function($routeProvider, $location, transitionTo) { - - function transitionListener(routeName, model, e) { - e.stopPropagation(); - e.preventDefault(); - transitionTo(routeName, model); - } - return { - restrict: 'A', - scope: { - routeName: '@linkTo', - model: '&' - }, - link: function (scope, element, attrs) { - - var listener; - - scope.$watch(function() { - var model = scope.$eval(scope.model); - return model; - }, function(newValue) { - - var model = scope.$eval(scope.model); - scope.url = lookupRouteUrl(scope.routeName, $routeProvider.routes, model, $location.$$html5); - element.off('click', listener); - - listener = _.partial(transitionListener, scope.routeName, model); - - element.on('click', listener); - - element.attr('href', scope.url); - }, true); - - } - }; - } - ]; diff --git a/awx/ui/client/src/shared/route-extensions/lookup-route-url.js b/awx/ui/client/src/shared/route-extensions/lookup-route-url.js deleted file mode 100644 index 25e0b9b9b0..0000000000 --- a/awx/ui/client/src/shared/route-extensions/lookup-route-url.js +++ /dev/null @@ -1,53 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export function lookupRouteUrl(name, routes, models, html5Mode) { - var route = _.find(routes, {name: name}); - - if (angular.isUndefined(route)) { - throw "Unknown route " + name; - } - - var routeUrl = route.originalPath; - - if (!angular.isUndefined(models) && angular.isObject(models)) { - var match = routeUrl.match(route.regexp); - var keyMatchers = match.slice(1); - - routeUrl = - keyMatchers.reduce(function(url, keyMatcher) { - var value; - var key = keyMatcher.replace(/^:/, ''); - - var model = models[key]; - - if (angular.isArray(model)) { - value = _.compact(_.pluck(model, key)); - - if (_.isEmpty(value)) { - value = _.pluck(model, 'id'); - } - - value = value.join(','); - } else if (angular.isObject(model)) { - value = model[key]; - - if (_.isEmpty(value)) { - value = model.id; - } - } - - return url.replace(keyMatcher, value); - }, routeUrl); - - } - - if (!html5Mode) { - routeUrl = '#' + routeUrl; - } - - return routeUrl; -} diff --git a/awx/ui/client/src/shared/route-extensions/main.js b/awx/ui/client/src/shared/route-extensions/main.js deleted file mode 100644 index 859a3a2969..0000000000 --- a/awx/ui/client/src/shared/route-extensions/main.js +++ /dev/null @@ -1,34 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import linkTo from './link-to.directive'; -import transitionTo from './transition-to.factory'; -import modelListener from './model-listener.config'; - -/** - * @ngdoc overview - * @name routeExtensions - * @description - * - * # routeExtensions - * - * Adds a couple useful features to ngRoute: - * - Adds a `name` property to route objects; used to identify the route in transitions & links - * - Adds the ability to pass model data when clicking a link that goes to a route - * - Adds a directive that generates a route's URL from the route name & given models - * - Adds the ability to specify models in route resolvers - * - * ## Usage - * - * If you need to generate a link to a route, then use the {@link routeExtensions.directive:linkTo `linkTo directive`}. If you need to transition to a route in JavaScript code, then use the {@link routeExtensions.factory:transitionTo `transitionTo service`}. - * -*/ -export default - angular.module('routeExtensions', - ['ngRoute']) - .factory('transitionTo', transitionTo) - .run(modelListener) - .directive('linkTo', linkTo); diff --git a/awx/ui/client/src/shared/route-extensions/model-listener.config.js b/awx/ui/client/src/shared/route-extensions/model-listener.config.js deleted file mode 100644 index 92567cae03..0000000000 --- a/awx/ui/client/src/shared/route-extensions/model-listener.config.js +++ /dev/null @@ -1,29 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {wrapDelegate} from './route-params.decorator'; - -export default - [ '$rootScope', - '$routeParams', - function($rootScope, $routeParams) { - $rootScope.$on('$routeChangeStart', function(e, newRoute) { - wrapDelegate(newRoute); - }); - - $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { - if (angular.isUndefined(newRoute.model)) { - var keys = Object.keys(newRoute.params); - var models = keys.reduce(function(model, key) { - model[key] = newRoute.locals[key]; - return model; - }, {}); - - $routeParams.model = models; - } - }); - } - ]; diff --git a/awx/ui/client/src/shared/route-extensions/route-params.decorator.js b/awx/ui/client/src/shared/route-extensions/route-params.decorator.js deleted file mode 100644 index 7702031c4b..0000000000 --- a/awx/ui/client/src/shared/route-extensions/route-params.decorator.js +++ /dev/null @@ -1,22 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export function wrapDelegate($delegate) { - $delegate.hasModelKey = function hasModelKey(key) { - return $delegate.params.hasOwnProperty('model') && - $delegate.params.model.hasOwnProperty(key); - }; - - return $delegate; -} - -export default - [ '$provide', - function($provide) { - $provide.decorator('$route', wrapDelegate); - - } - ]; diff --git a/awx/ui/client/src/shared/route-extensions/transition-to.factory.js b/awx/ui/client/src/shared/route-extensions/transition-to.factory.js deleted file mode 100644 index aeec18e43e..0000000000 --- a/awx/ui/client/src/shared/route-extensions/transition-to.factory.js +++ /dev/null @@ -1,137 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {lookupRouteUrl} from './lookup-route-url'; - -/** - * @ngdoc service - * @name routeExtensions.service:transitionTo - * @description - * The `transitionTo` service generates a URL given a route name and model parameters, then - * updates the browser's URL via `$location.path`. Use this in situations where you cannot - * use the `linkTo` directive, for example to redirect the user after saving an object. - * - * @param {string} routeName The name of the route whose URL you want to redirect to (corresponds - * name property of route) - * @param {object} model The model you want to use to generate the URL and be passed to the new - * route. This object follows a strict key/value naming convention where - * the keys match the parameters listed in the route's URL. For example, - * a URL of `/posts/:id` would require a model object like: `{ id: post }`, - * where `post` is the object you want to pass to the new route. - * - * **N.B.** The below example currently won't run. It's included to show an example of using - * the `transitionTo` function within code. In order for this to run, we will need to run - * the code in an iframe (using something like `dgeni` instead of `grunt-ngdocs`). - * - * @example - * - - - angular.module('transitionToExample', - ['ngRoute', - 'routeExtensions' - ]) - .config(function($routeProvider, $locationProvider) { - $routeProvider - .when('/post/:id', - { name: 'post', - template: '

    {{post.title}}

    {{post.body}}

    ' - }); - - $locationProvider.html5Mode(true); - $locationProvider.hashPrefix('!'); - }) - .controller('post', ['$scope', function($scope) { - - }]) - .controller('postForm', ['$scope', 'transitionTo', function($scope, transitionTo) { - $scope.post = { - id: 1, - title: 'A post', - body: 'Some text' - }; - - $scope.savePost = function() { - transitionTo('post', { id: $scope.post }); - } - }]); -
    - -
    - Edit Post - - - - -
    -
    - -
    - - */ - -function safeApply(fn, $rootScope) { - var currentPhase = $rootScope.$$phase; - - if (currentPhase === '$apply' || currentPhase === '$digest') { - fn(); - } else { - $rootScope.$apply(fn); - } -} - -export default - [ '$location', - '$rootScope', - '$route', - '$q', - function($location, $rootScope, $route, $q) { - return function(routeName, model) { - var deferred = $q.defer(); - var url = lookupRouteUrl(routeName, $route.routes, model, true); - - var offRouteChangeStart = - $rootScope.$on('$routeChangeStart', function(e, newRoute) { - if (newRoute.$$route.name === routeName) { - deferred.resolve(newRoute, model); - newRoute.params.model = model; - } - - offRouteChangeStart(); - }); - - var offRouteChangeSuccess = - $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { - if (newRoute.$$route.name === routeName) { - deferred.resolve(newRoute); - } - - offRouteChangeSuccess(); - }); - - var offRouteChangeError = - $rootScope.$on('$routeChangeError', function(e, newRoute, previousRoute, rejection) { - if (newRoute.$$route.name === routeName) { - deferred.reject(newRoute, previousRoute, rejection); - } - - offRouteChangeError(); - }); - - safeApply(function() { - $location.path(url); - }, $rootScope); - - return deferred; - }; - } - ]; diff --git a/awx/ui/client/src/shared/stateExtender.provider.js b/awx/ui/client/src/shared/stateExtender.provider.js new file mode 100644 index 0000000000..6499418993 --- /dev/null +++ b/awx/ui/client/src/shared/stateExtender.provider.js @@ -0,0 +1,17 @@ + +export default function($stateProvider){ + this.$get = function(){ + return { + addState: function(state) { + $stateProvider.state(state.name , { + url: state.route, + controller: state.controller, + templateUrl: state.templateUrl, + resolve: state.resolve, + params: state.params, + ncyBreadcrumb: state.ncyBreadcrumb + }); + } + }; + }; +} diff --git a/awx/ui/client/src/system-tracking/main.js b/awx/ui/client/src/system-tracking/main.js index 675f7e62fe..ae1ec2a51b 100644 --- a/awx/ui/client/src/system-tracking/main.js +++ b/awx/ui/client/src/system-tracking/main.js @@ -22,8 +22,6 @@ export default dataServices.name ]) .controller('systemTracking', controller) - .config(['$routeProvider', function($routeProvider) { - var url = route.route; - delete route.route; - $routeProvider.when(url, route); + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); }]); diff --git a/awx/ui/client/src/system-tracking/system-tracking.controller.js b/awx/ui/client/src/system-tracking/system-tracking.controller.js index 1cf8e787d0..2df9b46941 100644 --- a/awx/ui/client/src/system-tracking/system-tracking.controller.js +++ b/awx/ui/client/src/system-tracking/system-tracking.controller.js @@ -11,24 +11,26 @@ import FactTemplate from './compare-facts/fact-template'; function controller($rootScope, $scope, - $routeParams, + $stateParams, $location, $q, + $state, moduleOptions, + inventory, + hosts, getDataForComparison, waitIndicator, moment, _) { - // var inventoryId = $routeParams.id; - var hostIds = $routeParams.hosts.split(','); - var hosts = $routeParams.model.hosts; - var moduleParam = $routeParams.module || 'packages'; + // var inventoryId = $stateParams.id; + var hostIds = $stateParams.hostIds.split(','); + var moduleParam = $stateParams.module || 'packages'; $scope.compareMode = hostIds.length === 1 ? 'single-host' : 'host-to-host'; - $scope.hostIds = $routeParams.hosts; - $scope.inventory = $routeParams.model.inventory; + $scope.hostIds = $stateParams.hosts; + $scope.inventory = inventory; $scope.noModuleData = false; // this means no scans have been run @@ -314,10 +316,13 @@ function controller($rootScope, export default [ '$rootScope', '$scope', - '$routeParams', + '$stateParams', '$location', '$q', + '$state', 'moduleOptions', + 'inventory', + 'hosts', 'getDataForComparison', 'Wait', 'moment', diff --git a/awx/ui/client/src/system-tracking/system-tracking.partial.html b/awx/ui/client/src/system-tracking/system-tracking.partial.html index 01460f49a8..85580ac630 100644 --- a/awx/ui/client/src/system-tracking/system-tracking.partial.html +++ b/awx/ui/client/src/system-tracking/system-tracking.partial.html @@ -1,9 +1,4 @@
    - - - - -

    @@ -12,11 +7,6 @@

    - - - - -
    diff --git a/awx/ui/client/src/system-tracking/system-tracking.route.js b/awx/ui/client/src/system-tracking/system-tracking.route.js index e5108a8c9d..0149587769 100644 --- a/awx/ui/client/src/system-tracking/system-tracking.route.js +++ b/awx/ui/client/src/system-tracking/system-tracking.route.js @@ -8,18 +8,20 @@ import {templateUrl} from '../shared/template-url/template-url.factory'; export default { name: 'systemTracking', - route: '/inventories/:inventory/system-tracking/:hosts', + route: '/inventories/:inventoryId/system-tracking/:hostIds', controller: 'systemTracking', templateUrl: templateUrl('system-tracking/system-tracking'), + params: {hosts: null, inventory: null}, reloadOnSearch: false, resolve: { moduleOptions: [ 'getModuleOptions', 'lodashAsPromised', 'ProcessErrors', - '$route', - function(getModuleOptions, _, ProcessErrors, $route) { - var hostIds = $route.current.params.hosts.split(','); + '$stateParams', + function(getModuleOptions, _, ProcessErrors, $stateParams) { + + var hostIds = $stateParams.hostIds.split(','); var data = getModuleOptions(hostIds[0]) @@ -37,17 +39,18 @@ export default { } ], inventory: - [ '$route', + [ '$stateParams', '$q', 'Rest', 'GetBasePath', 'ProcessErrors', - function($route, $q, rest, getBasePath, ProcessErrors) { - if ($route.current.hasModelKey('inventory')) { - return $q.when($route.current.params.model.inventory); + function($stateParams, $q, rest, getBasePath, ProcessErrors) { + + if ($stateParams.inventory) { + return $q.when($stateParams.inventory); } - var inventoryId = $route.current.params.inventory; + var inventoryId = $stateParams.inventoryId; var url = getBasePath('inventory') + inventoryId + '/'; rest.setUrl(url); @@ -64,17 +67,17 @@ export default { } ], hosts: - [ '$route', + [ '$stateParams', '$q', 'Rest', 'GetBasePath', 'ProcessErrors', - function($route, $q, rest, getBasePath, ProcessErrors) { - if ($route.current.hasModelKey('hosts')) { - return $q.when($route.current.params.model.hosts); + function($stateParams, $q, rest, getBasePath, ProcessErrors) { + if ($stateParams.hosts) { + return $q.when($stateParams.hosts); } - var hostIds = $route.current.params.hosts.split(','); + var hostIds = $stateParams.hostIds.split(','); var hosts = hostIds.map(function(hostId) { diff --git a/awx/ui/client/src/widgets/Stream.js b/awx/ui/client/src/widgets/Stream.js index 5cbd299edf..b793d0bab1 100644 --- a/awx/ui/client/src/widgets/Stream.js +++ b/awx/ui/client/src/widgets/Stream.js @@ -67,8 +67,8 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } ]) -.factory('HideStream', ['LoadBreadCrumbs', - function (LoadBreadCrumbs) { +.factory('HideStream', [ + function () { return function () { // Remove the stream widget @@ -92,62 +92,6 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti 'min-height': 0 }); //let the parent height go back to normal }, 500); - - LoadBreadCrumbs(); - }; - } -]) - -.factory('StreamBreadCrumbs', ['$rootScope', '$location', - function ($rootScope, $location) { - return function () { - // Load the breadcrumbs array. We have to do things a bit different than Utilities.LoadBreadcrumbs. - // Rather than botch that all up, we'll do our own thing here. - $rootScope.breadcrumbs = []; - var path, title, i, j, paths = $location.path().split('/'), - capitalizeTitle = function(title) { - return title.split("_") - .map(function(title) { - return title.charAt(0).toUpperCase() + title.slice(1); - }).join(" "); - }; - paths.splice(0, 1); - for (i = 0; i < paths.length; i++) { - if (/^\d+/.test(paths[i])) { - path = ''; - title = ''; - for (j = 0; j <= i; j++) { - path += '/' + paths[j]; - } - for (j = 0; j < $rootScope.crumbCache.length; j++) { - if ($rootScope.crumbCache[j].path === path) { - title = $rootScope.crumbCache[j].title; - break; - } - } - if (!title) { - title = paths[i - 1].substr(0, paths[i - 1].length - 1); - title = capitalizeTitle(title); - title = (title === 'Inventorie') ? 'Inventory' : title; - } - } else { - path = ''; - title = ''; - if (i > 0) { - for (j = 0; j <= i; j++) { - path += '/' + paths[j]; - } - } else { - path = '/' + paths[i]; - } - title = paths[i]; - title = capitalizeTitle(title); - } - $rootScope.breadcrumbs.push({ - path: path, - title: title - }); - } }; } ]) @@ -393,9 +337,9 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti .factory('Stream', ['$rootScope', '$location', 'Rest', 'GetBasePath', 'ProcessErrors', 'Wait', 'StreamList', 'SearchInit', 'PaginateInit', 'generateList', 'FormatDate', 'ShowStream', 'HideStream', 'BuildDescription', 'FixUrl', 'BuildUrl', - 'ShowDetail', 'StreamBreadCrumbs', 'setStreamHeight', 'Find', 'Store', + 'ShowDetail', 'setStreamHeight', 'Find', 'Store', function ($rootScope, $location, Rest, GetBasePath, ProcessErrors, Wait, StreamList, SearchInit, PaginateInit, GenerateList, - FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, BuildUrl, ShowDetail, StreamBreadCrumbs, setStreamHeight, + FormatDate, ShowStream, HideStream, BuildDescription, FixUrl, BuildUrl, ShowDetail, setStreamHeight, Find, Store) { return function (params) { @@ -440,16 +384,6 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // Add a container for the stream widget $('#main-view').append("
    "); - StreamBreadCrumbs(); - - // Fix inventory name. The way we're doing breadcrumbs doesn't support bind variables. - if (inventory_name) { - itm = Find({ list: $rootScope.breadcrumbs, key: 'title', val: '{{ inventory.name }}' }); - if (itm) { - itm.title = inventory_name; - } - } - ShowStream(); // Generate the list diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 53bc6ba1f5..8b529e3b3e 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -46,7 +46,7 @@
    -
    +
    @@ -171,7 +171,8 @@ save one fact scan (snapshot) per time window (frequency). For example, facts older than 30 days are purged, while one weekly fact scan is kept.
    - Caution: Setting both numerical variables to "0" will delete all facts.
    + Caution: Setting both numerical variables to "0" will delete all facts.
    +
    -
    +
    @@ -220,7 +221,7 @@

    working...

    - +