diff --git a/.gitignore b/.gitignore index 895766b..9b15570 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,10 @@ logs results lib node_modules +coverage components bower_components npm-debug.log src/*.tpls.js +.idea/ +angular-wizard.iml diff --git a/.travis.yml b/.travis.yml index 1426a7a..dd79aa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,19 @@ language: node_js node_js: - - "0.10" + - "5" before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start + - npm install -g bower + - npm install -g grunt + +script: + - npm test + +after_success: + - npm run coveralls + - npm run codacy + +notifications: + - email: false + +sudo: false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..01e4922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,77 @@ + +# 1.1.1 (2017-06-07) + +- Update README.md to expose $broadcasts the Wizard offers. + + + +# 1.1.0 (2017-06-07) + +- Update README.md to explain Edit Mode. +- Add `wz-heading-title` attribute to each step. + + + +# 1.0.2 (2017-06-06) + +- Update README.md to update Repository URLs + + + +# 1.0.2 (2017-06-06) + +- Update CDN instructions on README.md + + + +# 1.0.0 (2017-06-06) + +- `canExit` and `canEnter` functions may now be strings. +- `setEditMode(mode)` methoda added to wizard so a user can explicitly set the edit mode on a wizard. +- Main files in bower.json no longer reference minified files. +- Travis CI config files updated to allow CI automated testing to pass +- npm package versions updated. + +## Updates + +- Fix for Issue [#210](https://github.com/angular-wizard/angular-wizard/issues/210). +- Newly enabled steps are set to complete. + + +# 0.10.0 (2016-12-22) + +## Updates + +- Fix for Issue [#210](https://github.com/angular-wizard/angular-wizard/issues/210). +- Newly enabled steps are set to complete. + + +# 0.9.0 (2016-09-19) + +## Updates + +- Fix for Issue [#184](https://github.com/angular-wizard/angular-wizard/issues/184). +- Step title updates on change. + + +# 0.8.0 (2016-09-12) + +## Updates + +- **wizard-order:** the `wizard-order` attribute affects the order in which the steps should be in. If no order or duplicated order it will add the step to the end. + + +# 0.7.1 (2016-09-07) + +## Updates + +- **indicators-position:** the `indicators-position` attribute has been added to `wizard` directive to allow you to specify "top" or "bottom" for defualt step positions. +- Created $destroy event that remove steps from wizard for wizards built using `ng-repeat` for step creation +- Wrapped goTo method in $timeout to fix bug where step recently enabled weren't being seen by `getEnabledSteps` + + +# 0.6.1 (2015-12-31) + +## Updates + +- **wz-title:** the `title` attribute has been changed to `wz-title`. Examples in README have been updated. diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 73a2b43..01c4352 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -1,17 +1,37 @@ -In order to Contribute just git clone the repository and then run: +# Contributing Guidelines -``` -npm install grunt-cli --global -npm install -grunt -``` +First of all, thank you for considering to contribute to the Angular Wizard project. +The community is important to us and we appreciate your time and effort to help pushing towards a better generic tree component. -Be sure to have PhantomJS installed as Karma tests use it. Otherwise, in mac just run +## Submitting a PR -``` -brew install phantomjs -``` +As an attempt to make the process of contributing more pleasant for you and the reviewers, we've created a checklist. +Please make sure your PR confirms to critera listed below. -All changes must be done in src/restangular.js and then after running grunt all changes will be submited to dist/ +- Use a descriptive title using proper grammar (this is important because the same text will be used for the changelog) +- Make sure the build passes `grunt build` +- Do not include files from the `dist/` directory (These are created when a new release is made. Adding them to every PR will result in merge conflicts for other PR's.) -Please submit a Pull Request or create issues for anything you want :). +## Accepting a PR + +1. Manually review PR in the 'Files Changed' tab of the GitHub PR section +2. Ensure Travis build passing +3. Review Coveralls and Codacy reports. +A fail does not necessarily mean a PR should be rejected, but it is a red flag that you should take a close look at the report. +4. Merge the PR. If the PR is simple and good as-is you can merge it using GitHub's web UI. If the PR needs some amending, or you want to test the changes, perform a command line merge (the 'commandline instructions' link gives the commands). +5. Test the PR locally and run the tests (grunt test) +6. Make any additional changes you need (update README, update example site, etc) +7. Push the changes + +## Creating a release + +1. Run the tests (`$ grunt test`) +3. Update CHANGELOG.md with release notes for the changes in the this release +4. Update version with `grunt bump` add the type flag otherwise a "patch" version change will be applied. e.g. `grunt bump --type=minor` +5. Run `$ grunt build` and commit, this time including the new dist files. +6. Tag the commit as a new release (like v2.18.0) (eg. `$ git tag -a v0.0.2 -m "Release version 0.0.2"`) +7. Push all the changes including tags (eg. `$ git push --tags`) +8. Publish to npm with `$ npm publish ./` +9. Add a new [Release in GitHub](https://github.com/angular-wizard/angular-wizard/releases) + +Our intention is to keep working on this document. Happy coding! \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 8ed8696..10f4f00 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,4 +1,4 @@ - 'use strict'; +'use strict'; module.exports = function(grunt) { @@ -55,6 +55,26 @@ module.exports = function(grunt) { } } }, + less: { + build: { + options: { + paths: ["<%= dirs.dest %>"], + cleancss: true, + }, + files: {"<%= dirs.dest %>/<%= pkg.name %>.css": "src/angular-wizard.less"} + } + }, + cssmin: { + options: { + shorthandCompacting: false, + roundingPrecision: -1 + }, + target: { + files: { + '<%= dirs.dest %>/<%= pkg.name %>.min.css': ['<%= dirs.dest %>/<%= pkg.name %>.css'] + } + } + }, copy: { less_files: { files: [ @@ -79,7 +99,7 @@ module.exports = function(grunt) { }, src: [ 'src/*.html' ], dest: 'src/<%= pkg.name %>.tpls.js' - }, + } }, uglify: { options: { @@ -98,11 +118,6 @@ module.exports = function(grunt) { singleRun: true, autoWatch: false }, - travis: { - singleRun: true, - autoWatch: false, - browsers: ['Firefox'] - }, dev: { autoWatch: true } @@ -119,20 +134,15 @@ module.exports = function(grunt) { // Load the plugin that provides the "uglify" task. grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-bower-task'); - grunt.renameTask("bower", "bowerInstall"); - - grunt.loadNpmTasks('grunt-karma'); grunt.loadNpmTasks('grunt-karma'); - grunt.loadNpmTasks('grunt-conventional-changelog'); - - grunt.loadNpmTasks('grunt-recess'); + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-copy'); - grunt.loadNpmTasks('grunt-ngmin'); + grunt.loadNpmTasks('grunt-ng-annotate'); grunt.loadNpmTasks('grunt-html2js'); @@ -144,7 +154,8 @@ module.exports = function(grunt) { 'clean', 'bowerInstall', 'copy', - 'recess', + 'less', + 'cssmin', 'html2js', 'concat', 'uglify', diff --git a/README.md b/README.md index 0bd9ad2..e932f42 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,26 @@ # Angular-Wizard -Angular-wizard is a component that will make it easy for you to create wizards in your app. You can check a running example of the wizard [by clicking here](http://mgonto.github.io/angular-wizard/) +[![Build Status](https://travis-ci.org/angular-wizard/angular-wizard.svg?branch=master)](https://travis-ci.org/angular-wizard/angular-wizard) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8a488d7e632d4d1ea973bfcd25ea15a0)](https://www.codacy.com/app/jc_2/angular-wizard?utm_source=github.com&utm_medium=referral&utm_content=angular-wizard/angular-wizard&utm_campaign=Badge_Grade) [![Coverage Status](https://coveralls.io/repos/github/angular-wizard/angular-wizard/badge.svg?branch=master)](https://coveralls.io/github/angular-wizard/angular-wizard?branch=master) [![npm version](https://badge.fury.io/js/angular-wizard.svg)](https://badge.fury.io/js/angular-wizard) [![PayPal donate button](http://img.shields.io/paypal/donate.png?color=yellow)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=martin%40gon%2eto&lc=US&item_name=Martin%20Gontovnikas¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted "Donate once-off to this project using PayPal") +[![Donate on Gratipay](http://img.shields.io/gratipay/mgonto.svg)](https://gratipay.com/mgonto/) + +Angular-wizard is a component that will make it easy for you to create wizards in your app. You can check a running example of the wizard [by clicking here](http://angular-wizard.github.io/angular-wizard/) # How do I add this to my project? You can download this by: -* Using bower and running bower install angular-wizard -* Using npm and running npm install angular-wizard +* Using bower and running `bower install angular-wizard` +* Using npm and running `npm install angular-wizard` * Downloading it manually by getting the files from the dist folder * Using JsDelivr CDN files: ````html - - + + - - + + ```` The dist folder contains the following files: @@ -27,7 +30,7 @@ The dist folder contains the following files: * LESS file with styles for the directive. If you have less in your project, I recommend using the less instead of the CSS since it has variables to configure Wizard colors. # Dependencies -Angular-wizard depends on Angular and Lodash (or Underscore). +Angular-wizard depends on Angular. # Starter Guide @@ -44,21 +47,21 @@ angular.module('your-app', ['mgo-angular-wizard']); Now, in some HTML for a controller, you can just add a wizard as follows: ````html - - + +

This is the first step

Here you can use whatever you want. You can use other directives, binding, etc.

-
- + +

Continuing

You have continued here!

-
- + +

Even more steps!!

-
+
```` @@ -69,18 +72,30 @@ This will look like the following when you're in the second step: Let's go step by step to see how this works. 1) You need to declare a master `wizard` directive. This wizard directive, has the following options as attributes: -* **on-finish**: Here you can put a function to be called when the wizard is finished. The syntaxis here is very similar to `ng-click` +* **on-finish**: Here you can put a function to be called when the wizard is finished. The syntax here is very similar to `ng-click` +* **on-cancel**: Here you can put a function to be called when the wizard is cancelled. The syntax here is very similar to `ng-click` * **name**: The name of the wizard. By default, it's called "Default wizard". It's used for the `WizardHandler` which we'll explain later. -* **edit-mode**: If set to true, this will set the wizard as edit mode. Edit mode means that all steps have been completed byt he can modify any of them now. Defaults to false. +* **edit-mode**: If set to true, this will set the wizard as edit mode. Edit mode means that all steps have been completed and the user can now navigate to and modify any step. Defaults to false. * **hide-indicators**: If set to true, the indicators in the bottom of the page showing the current page and allowing navigation for the wizard will be hidden. Defaults to false. * **current-step**: You need to set here a property from your scope (similar to `ng-model`) and that property will always have the name of the current step being shown on the screen. - -2) Inside the wizard, we can have as many steps as we want. Each step MUST have a title which is going to be used to identify it. Inside each step, we can put whatever we want. Other directives, bindings, controls, forms, etc. +* **template**: Path to a custom template. +* **indicators-position**: Can use "top" or "bottom" to specify default position for steps. + +2) Inside the wizard, we can have as many steps as we want. Each step MUST have a title which is going to be used to identify it. Inside each step, we can put whatever we want. Other directives, bindings, controls, forms, etc. Each step can have the following attributes (we will go into detail on each further below): +* **wz-title:** A unique title used for identifying each step. +* **wz-heading-title** A heading title display above step indicators +* **canenter** +* **canexit** +* **wz-disabled** +* **description:** A description available to use in each step's UI. +* **wz-data** Data you wish to make available to the steps scope. +* **wz-order** The order in which the steps should be in. If no order or duplicated order it will add the step to the end. 3) Inside the step, we now see a button which has a `wz-next` attribute. That means that clicking that button will send the user to the next step of wizard. Similar to `wz-next`, we have the following attributes: * **wz-previous**: Goes to the previous step -* **wz-cancel**: Goes back to the first step +* **wz-cancel**: Calls on-cancel if defined, otherwise the default action is to go back to the first step * **wz-finish**: Finishes the wizard and calls the on-finish later on. It's important to note that if we're in the last step and we put `wz-next` it'll be the same as putting `wz-finish` as the wizard will know we're at the last screen. +* **wz-reset**: This will reset the wizard meaning bring the user to the first step and reset each step to being incomplete. All of this attributes can receive an optional function to be called before changing the step. Something like: @@ -90,17 +105,98 @@ All of this attributes can receive an optional function to be called before chan In this case, the `setMode` function will be called before going to the next step. +## Wizard Dynamic Steps +A step can be conditionally disabled and may change at any time either adding it or removing it from the wizard step flow. + + **Example** + +HTML +````html + + +

This is the first step

+

Here you can use whatever you want. You can use other directives, binding, etc.

+ +
+ +

Continuing

+

You have continued here!

+ +
+ +

Even more steps!!

+ +
+
+```` +Controller +````javascript +//this will cause the step to be hidden +$scope.disabled = 'true'; +```` + + +## Wizard Step Validation +The wzStep directive has the following options as attributes: +* **canexit**: Here you can reference a function from your controller. If this attribute is listed the function must return true in order for the wizard to move to the next step. Promises are supported but must resolve with a thruthy value. If it is ommitted no validation will be required. +* **canenter**: Here you can reference a function from your controller. If this attribute is listed the function must return true in order for the wizard to move into this step. Promises are supported but must resolve with a thruthy value. If it is ommitted no validation will be required. + + **Example** + +HTML +````html + + +

This is the first step

+

Here you can use whatever you want. You can use other directives, binding, etc.

+ +
+ +

Continuing

+

You have continued here!

+ +
+ +

Even more steps!!

+ +
+
+```` +Controller +````javascript +$scope.enterValidation = function(){ + return true; +}; + +$scope.exitValidation = function(){ + return true; +}; +//example using context object +$scope.exitValidation = function(context){ + return context.firstName === "Jacob"; +} +//example using promises +$scope.exitValidation = function(){ + var d = $q.defer() + $timeout(function(){ + return d.resolve(true); + }, 2000); + return d.promise; +} +```` +If a step requires information from a previous step to populate itself you can access this information through the `context` object. The context object is automatically passed in as an argument into your canexit and canenter methods. You can access the context objext from your controller via: `WizardHandler.wizard().context` + ## Manipulating the wizard from a service -There're some times where we actually want to manipulate the wizard from the controller instead of from the HTML. +There are some times where we actually want to manipulate the wizard from the controller instead of from the HTML. For those cases, we can inject the `WizardHandler` to our controller. The main function of this service is the `wizard(name)` which will let you get the wizard to manipulate it. If you have just one wizard in the screen and you didn't set a name to it, you can just call it as `wizard()`. Let's see an example: ````html - + - + ```` ````js @@ -113,14 +209,33 @@ $scope.changeLabelAndGoNext = function() { In this case, we're changing a label and moving forward on the steps. The functions available in the `wizard()` are: -* **next**: Goes to the next step -* **previous**: Goes to the previous step +* **next**: Goes to the next step. +* **previous**: Goes to the previous step. +* **cancel**: Goes to the previous step. +* **reset**: Goes to the first step and resets all steps to incomplete. * **finish**: Finishes the wizard. * **goTo(number|title)**: This goes to the indicated step. It can receive either the number of the step (starting from 0) or the title of the step to go to. +* **currentStepNumber()**: This returns a Number which is the current step number you are on. +* **currentStepTitle()**: This returns a String which is the title of the step you are on. +* **currentStepDescription()**: This returns a String which is the description of the step you are on. +* **currentStep()**: This returns an Object which is the current step you are on. +* **totalStepCount()**: This returns an Number which is the total number of **enabled** steps. +* **getEnabledSteps()**: This returns an Array which is the **enabled** steps. +* **setEditMode(boolean)**: Set the edit mode of the wizard. Setting editMode to `true` will make ALL steps accessible, setting edit mode to `false` will make all steps with an index lower than the latest "completed" step accessible. + +## Events +Angular Wizard emits the following events on the `scope` and pass an object with the step info and the index as argument: +- `wizard:stepChanged`: When succeed changing the current step +- `wizard:stepChangeFailed`: When changing step and either fails `canexit` of leaving step or `canenter` of arriving step. +````javascript +$scope.$on('wizard:stepChanged',function(event, args) { + console.log(args); +} +```` ## Navigation bar -The navigation bar shown below works in the following way: +Any changes you wish to make to the navigation bar can be done by overwritting the CSS. Because everyone wants the navigation bar in a different location and in a different style we have provided a default which you can change via your own HTML and CSS. The navigation bar shown below works in the following way: * Completed steps are painted as green * Current step is painted as dark grey @@ -128,24 +243,31 @@ The navigation bar shown below works in the following way: * Editing step (Modifying a step already completed in the past) is painted as red * You can click in any completed step to go back to that step. You can't click in the current step nor in the future ones unless you've already completed a future step before (for example in EditMode all steps are completed by default) -All of those colors are variables in the `angular-wizard.less`. You can easily change them by changing the colors in that file +### Color Definitions +All of those colors are variables in the `angular-wizard.less`. You can easily change them by importing the `angular-wizard.less` file into your own less file(s) and change the variables. +The available variables are: +* @wz-color-default +* @wz-color-done +* @wz-color-current +* @wz-color-editing # Contributors -@sebazelonka helped me with all fo the styles of the Wizard. +* [@sebazelonka](https://github.com/sebazelonka) helped me with all of the styles in the Wizard. +* [@jacobscarter](https://github.com/jacobscarter) is helping with manteinance, PRS merging and adding new features # Live sample -You can check out a live sample of the Wizard [clicking here](http://mgonto.github.io/angular-wizard/) +You can check out a live sample of the Wizard [clicking here](http://angular-wizard.github.io/angular-wizard/) # Releases Notes -Releases notes are together with releases in GitHub at: https://github.com/mgonto/angular-wizard/releases +Releases notes are together with releases in GitHub at: https://github.com/angular-wizard/angular-wizard/releases # License The MIT License -Copyright (c) 2013 Martin Gontovnikas http://www.gon.to/ +Copyright (c) 2013-2014 Martin Gontovnikas http://www.gon.to/ 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: @@ -154,5 +276,5 @@ The above copyright notice and this permission notice shall be included in all c 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. -[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/mgonto/angular-wizard/trend.png)](https://bitdeli.com/free "Bitdeli Badge") +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/angular-wizard/angular-wizard/trend.png)](https://bitdeli.com/free "Bitdeli Badge") diff --git a/bower.json b/bower.json index 7f95dc3..54cfbe0 100644 --- a/bower.json +++ b/bower.json @@ -1,8 +1,11 @@ { "name": "angular-wizard", - "version": "0.2.0", - "main": "./dist/angular-wizard.js", - "description": "Easy to use Wizard library for AngularJS", + "version": "1.1.1", + "main": [ + "./dist/angular-wizard.js", + "./dist/angular-wizard.css" + ], + "description": "Easy to use Wizard library for AngularJS ", "repository": { "type": "git", "url": "git://github.com/mgonto/angular-wizard.git" diff --git a/dist/angular-wizard.css b/dist/angular-wizard.css index af6b962..97efc9d 100644 --- a/dist/angular-wizard.css +++ b/dist/angular-wizard.css @@ -1,168 +1,138 @@ .steps-indicator { - position: absolute; + /* ---- steps quantity ---- */ right: 0; bottom: 0; left: 0; - height: 30px; - padding: 0; margin: 0; + padding: 20px 0 0 0; + height: 30px; list-style: none; + /* --- http://www.paulirish.com/2012/box-sizing-border-box-ftw/ ---- */ } - .steps-indicator:before { - position: absolute; - height: 1px; background-color: #e6e6e6; content: ''; + position: absolute; + height: 1px; } - .steps-indicator.steps-2:before { - right: calc(25%); - left: calc(25%); + left: calc(100% / 2 / 2); + right: calc(100% / 2 / 2); } - .steps-indicator.steps-3:before { - right: calc(16.666666666666668%); - left: calc(16.666666666666668%); + left: calc(100% / 3 / 2); + right: calc(100% / 3 / 2); } - .steps-indicator.steps-4:before { - right: calc(12.5%); - left: calc(12.5%); + left: calc(100% / 4 / 2); + right: calc(100% / 4 / 2); } - .steps-indicator.steps-5:before { - right: calc(10%); - left: calc(10%); + left: calc(100% / 5 / 2); + right: calc(100% / 5 / 2); } - .steps-indicator.steps-6:before { - right: calc(8.333333333333334%); - left: calc(8.333333333333334%); + left: calc(100% / 6 / 2); + right: calc(100% / 6 / 2); } - .steps-indicator.steps-7:before { - right: calc(7.142857142857143%); - left: calc(7.142857142857143%); + left: calc(100% / 7 / 2); + right: calc(100% / 7 / 2); } - .steps-indicator.steps-8:before { - right: calc(6.25%); - left: calc(6.25%); + left: calc(100% / 8 / 2); + right: calc(100% / 8 / 2); } - .steps-indicator.steps-9:before { - right: calc(5.555555555555555%); - left: calc(5.555555555555555%); + left: calc(100% / 9 / 2); + right: calc(100% / 9 / 2); } - .steps-indicator.steps-10:before { - right: calc(5%); - left: calc(5%); + left: calc(100% / 10 / 2); + right: calc(100% / 10 / 2); } - .steps-indicator * { -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } - .steps-indicator li { position: relative; float: left; + margin: 0; padding: 0; padding-top: 10px; - margin: 0; - line-height: 15px; text-align: center; + line-height: 15px; } - .steps-indicator li a { - font-weight: bold; color: #808080; text-decoration: none; text-transform: uppercase; - cursor: pointer; + font-weight: bold; transition: 0.25s; + cursor: pointer; } - .steps-indicator li a:before { position: absolute; top: -7px; - left: calc(43%); + left: calc(50% - 7px); width: 14px; height: 14px; - background-color: #e6e6e6; border-radius: 100%; + background-color: #e6e6e6; content: ''; transition: 0.25s; } - .steps-indicator li a:hover { color: #4d4d4d; } - .steps-indicator.steps-2 li { - width: calc(50%); + width: calc(100% / 2); } - .steps-indicator.steps-3 li { - width: calc(33.333333333333336%); + width: calc(100% / 3); } - .steps-indicator.steps-4 li { - width: calc(25%); + width: calc(100% / 4); } - .steps-indicator.steps-5 li { - width: calc(20%); + width: calc(100% / 5); } - .steps-indicator.steps-6 li { - width: calc(16.666666666666668%); + width: calc(100% / 6); } - .steps-indicator.steps-7 li { - width: calc(14.285714285714286%); + width: calc(100% / 7); } - .steps-indicator.steps-8 li { - width: calc(12.5%); + width: calc(100% / 8); } - .steps-indicator.steps-9 li { - width: calc(11.11111111111111%); + width: calc(100% / 9); } - .steps-indicator.steps-10 li { - width: calc(10%); + width: calc(100% / 10); } - .steps-indicator.steps-11 li { - width: calc(9.090909090909092%); + width: calc(100% / 11); } - .steps-indicator li.default { pointer-events: none; } - .steps-indicator li.default a:hover { color: #808080; } - .steps-indicator li.current, .steps-indicator li.editing { pointer-events: none; } - .steps-indicator li.current a:before { background-color: #808080; } - .steps-indicator li.done a:before { background-color: #339933; } - .steps-indicator li.editing a:before { background-color: #ff0000; -} \ No newline at end of file +} diff --git a/dist/angular-wizard.js b/dist/angular-wizard.js index 113c150..6380ed4 100644 --- a/dist/angular-wizard.js +++ b/dist/angular-wizard.js @@ -1,177 +1,485 @@ -/** - * Easy to use Wizard library for AngularJS - * @version v0.2.0 - 2014-01-30 * @link https://github.com/mgonto/angular-wizard - * @author Martin Gontovnikas - * @license MIT License, http://www.opensource.org/licenses/MIT - */ -angular.module('templates-angularwizard', ['step.html', 'wizard.html']); - -angular.module("step.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("step.html", - "
\n" + - "
"); -}]); - -angular.module("wizard.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("wizard.html", - "
\n" + - "
\n" + - " \n" + - "
"); -}]); - -angular.module('mgo-angular-wizard', ['templates-angularwizard']); -angular.module('mgo-angular-wizard').directive('step', function() { +/** + * Easy to use Wizard library for Angular JS + * @version v1.1.1 - 2017-06-07 * @link https://github.com/mgonto/angular-wizard + * @author Martin Gontovnikas + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +angular.module('templates-angularwizard', ['step.html', 'wizard.html']); + +angular.module("step.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("step.html", + "
\n" + + "
"); +}]); + +angular.module("wizard.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("wizard.html", + "
\n" + + "

{{ selectedStep.wzHeadingTitle }}

\n" + + "\n" + + "
\n" + + " \n" + + "
\n" + + "
\n" + + ""); +}]); + +angular.module('mgo-angular-wizard', ['templates-angularwizard']); + +angular.module('mgo-angular-wizard').directive('wzStep', function() { return { - restrict: 'E', + restrict: 'EA', replace: true, transclude: true, scope: { - title: '@' + wzTitle: '@', + wzHeadingTitle: '@', + canenter : '=', + canexit : '=', + disabled: '@?wzDisabled', + description: '@', + wzData: '=', + wzOrder: '@?' }, require: '^wizard', - templateUrl: 'step.html', - link: function($scope, $element, $attrs, wizard) { + templateUrl: function(element, attributes) { + return attributes.template || "step.html"; + }, + link: function ($scope, $element, $attrs, wizard) { + $attrs.$observe('wzTitle', function (value) { + $scope.title = $scope.wzTitle; + }); + $scope.title = $scope.wzTitle; wizard.addStep($scope); + $scope.$on('$destroy', function(){ + wizard.removeStep($scope); + }); } - } + }; }); + +//wizard directive angular.module('mgo-angular-wizard').directive('wizard', function() { return { - restrict: 'E', + restrict: 'EA', replace: true, transclude: true, scope: { currentStep: '=', + onCancel: '&', onFinish: '&', hideIndicators: '=', editMode: '=', - name: '@' + name: '@', + indicatorsPosition: '@?' }, - templateUrl: 'wizard.html', - controller: ['$scope', '$element', 'WizardHandler', function($scope, $element, WizardHandler) { - + templateUrl: function(element, attributes) { + return attributes.template || "wizard.html"; + }, + + //controller for wizard directive, treat this just like an angular controller + controller: ['$scope', '$element', '$log', 'WizardHandler', '$q', '$timeout', function ($scope, $element, $log, WizardHandler, $q, $timeout) { + //setting default step position if none declared. + if ($scope.indicatorsPosition == undefined) { + $scope.indicatorsPosition = 'bottom'; + } + //this variable allows directive to load without having to pass any step validation + var firstRun = true; + //creating instance of wizard, passing this as second argument allows access to functions attached to this via Service WizardHandler.addWizard($scope.name || WizardHandler.defaultName, this); + $scope.$on('$destroy', function() { WizardHandler.removeWizard($scope.name || WizardHandler.defaultName); }); - + + //steps array where all the scopes of each step are added $scope.steps = []; - + + var stepIdx = function(step) { + var idx = 0; + var res = -1; + angular.forEach($scope.getEnabledSteps(), function(currStep) { + if (currStep === step) { + res = idx; + } + idx++; + }); + return res; + }; + + var stepByTitle = function(titleToFind) { + var foundStep = null; + angular.forEach($scope.getEnabledSteps(), function(step) { + if (step.wzTitle === titleToFind) { + foundStep = step; + } + }); + return foundStep; + }; + + + //update completed state for each step based on the editMode and current step number + var handleEditModeChange = function() { + var editMode = $scope.editMode; + if (angular.isUndefined(editMode) || (editMode === null)) return; + + //Set completed for all steps to the value of editMode + angular.forEach($scope.steps, function (step) { + step.completed = editMode; + }); + + //If editMode is false, set ONLY ENABLED steps with index lower then completedIndex to completed + if (!editMode) { + var completedStepsIndex = $scope.currentStepNumber() - 1; + angular.forEach($scope.getEnabledSteps(), function(step, stepIndex) { + if(stepIndex < completedStepsIndex) { + step.completed = true; + } + }); + } + }; + + //access to context object for step validation + $scope.context = {}; + + //watching changes to currentStep $scope.$watch('currentStep', function(step) { + //checking to make sure currentStep is truthy value if (!step) return; - - if ($scope.selectedStep && $scope.selectedStep.title !== $scope.currentStep) { - $scope.goTo(_.find($scope.steps, {title: $scope.currentStep})); + //setting stepTitle equal to current step title or default title + var stepTitle = $scope.selectedStep.wzTitle; + if ($scope.selectedStep && stepTitle !== $scope.currentStep) { + //invoking goTo() with step title as argument + $scope.goTo(stepByTitle($scope.currentStep)); } - }); - + + //watching steps array length and editMode value, if edit module is undefined or null the nothing is done + //if edit mode is truthy, then all steps are marked as completed $scope.$watch('[editMode, steps.length]', function() { - var editMode = $scope.editMode; - if (_.isUndefined(editMode) || _.isNull(editMode)) return; - - if (editMode) { - _.each($scope.steps, function(step) { - step.completed = true; - }); - } + handleEditModeChange(); }, true); - + + //called each time step directive is loaded this.addStep = function(step) { - $scope.steps.push(step); - if ($scope.steps.length === 1) { - $scope.goTo($scope.steps[0]); + var wzOrder = (step.wzOrder >= 0 && !$scope.steps[step.wzOrder]) ? step.wzOrder : $scope.steps.length; + //adding the scope of directive onto step array + $scope.steps[wzOrder] = step; + //if this step is the new first then goTo it + if ($scope.getEnabledSteps()[0] === step) { + //goTo first step + $scope.goTo($scope.getEnabledSteps()[0]); } - } - + }; + + //called each time step directive is destroyed + this.removeStep = function (step) { + var index = $scope.steps.indexOf(step); + if (index > 0) { + $scope.steps.splice(index, 1); + } + }; + + this.context = $scope.context; + + $scope.getStepNumber = function(step) { + return stepIdx(step) + 1; + }; + $scope.goTo = function(step) { - unselectAll(); - $scope.selectedStep = step; - if ($scope.currentStep) { - $scope.currentStep = step.title; + //if this is the first time the wizard is loading it bi-passes step validation + if(firstRun){ + //deselect all steps so you can set fresh below + unselectAll(); + $scope.selectedStep = step; + //making sure current step is not undefined + if (!angular.isUndefined($scope.currentStep)) { + $scope.currentStep = step.wzTitle; + } + //setting selected step to argument passed into goTo() + step.selected = true; + //emit event upwards with data on goTo() invoktion + $scope.$emit('wizard:stepChanged', {step: step, index: stepIdx(step)}); + //setting variable to false so all other step changes must pass validation + firstRun = false; + } else { + //createing variables to capture current state that goTo() was invoked from and allow booleans + var thisStep; + //getting data for step you are transitioning out of + if($scope.currentStepNumber() > 0){ + thisStep = $scope.currentStepNumber() - 1; + } else if ($scope.currentStepNumber() === 0){ + thisStep = 0; + } + //$log.log('steps[thisStep] Data: ', $scope.getEnabledSteps()[thisStep].canexit); + $q.all([canExitStep($scope.getEnabledSteps()[thisStep], step), canEnterStep(step)]).then(function(data) { + if(data[0] && data[1]){ + //deselect all steps so you can set fresh below + unselectAll(); + + //$log.log('value for canExit argument: ', $scope.currentStep.canexit); + $scope.selectedStep = step; + //making sure current step is not undefined + if(!angular.isUndefined($scope.currentStep)){ + $scope.currentStep = step.wzTitle; + } + //setting selected step to argument passed into goTo() + step.selected = true; + //emit event upwards with data on goTo() invoktion + $scope.$emit('wizard:stepChanged', {step: step, index: stepIdx(step)}); + //$log.log('current step number: ', $scope.currentStepNumber()); + } else { + $scope.$emit('wizard:stepChangeFailed', {step: step, index: _.indexOf($scope.getEnabledSteps(), step)}); + } + }); + } + }; + + function canEnterStep(step) { + var defer; + var canEnter; + //If no validation function is provided, allow the user to enter the step + if(step.canenter === undefined){ + return true; + } + //If canenter is a boolean value instead of a function, return the value + if(typeof step.canenter === 'boolean'){ + return step.canenter; + } + //If canenter is a string instead of a function, evaluate the function + if(typeof step.canenter === 'string'){ + var splitFunction = step.canenter.split('('); + canEnter = eval('$scope.$parent.' + splitFunction[0] + '($scope.context' + splitFunction[1]) + } else { + canEnter = step.canenter($scope.context); + } + //Check to see if the canenter function is a promise which needs to be returned + if(angular.isFunction(canEnter.then)){ + defer = $q.defer(); + canEnter.then(function(response){ + defer.resolve(response); + }); + return defer.promise; + } else { + return canEnter === true; } - step.selected = true; } - + + function canExitStep(step, stepTo) { + var defer; + var canExit; + //Exiting the step should be allowed if no validation function was provided or if the user is moving backwards + if(typeof(step.canexit) === 'undefined' || $scope.getStepNumber(stepTo) < $scope.currentStepNumber()){ + return true; + } + //If canexit is a boolean value instead of a function, return the value + if(typeof step.canexit === 'boolean'){ + return step.canexit; + } + //If canenter is a string instead of a function, evaluate the function + if(typeof step.canexit === 'string'){ + var splitFunction = step.canexit.split('('); + canExit = eval('$scope.$parent.' + splitFunction[0] + '($scope.context' + splitFunction[1]) + } else { + canExit = step.canexit($scope.context); + } + //Check to see if the canexit function is a promise which needs to be returned + if(angular.isFunction(canExit.then)){ + defer = $q.defer(); + canExit.then(function(response){ + defer.resolve(response); + }); + return defer.promise; + } else { + return canExit === true; + } + } + + $scope.currentStepNumber = function() { + //retreive current step number + return stepIdx($scope.selectedStep) + 1; + }; + + $scope.getEnabledSteps = function() { + return $scope.steps.filter(function(step){ + return step && step.disabled !== 'true'; + }); + }; + + //unSelect All Steps function unselectAll() { - _.each($scope.steps, function (step) { + //traverse steps array and set each "selected" property to false + angular.forEach($scope.getEnabledSteps(), function (step) { step.selected = false; }); + //set selectedStep variable to null $scope.selectedStep = null; } - - this.next = function(draft) { - var index = _.indexOf($scope.steps , $scope.selectedStep); - if (!draft) { + + //ALL METHODS ATTACHED TO this ARE ACCESSIBLE VIA WizardHandler.wizard().methodName() + + this.currentStepTitle = function(){ + return $scope.selectedStep.wzTitle; + }; + + this.currentStepDescription = function(){ + return $scope.selectedStep.description; + }; + + this.currentStep = function(){ + return $scope.selectedStep; + }; + + this.totalStepCount = function() { + return $scope.getEnabledSteps().length; + }; + + //Access to enabled steps from outside + this.getEnabledSteps = function(){ + return $scope.getEnabledSteps(); + }; + + //Access to current step number from outside + this.currentStepNumber = function(){ + return $scope.currentStepNumber(); + }; + //method used for next button within step + this.next = function(callback) { + var enabledSteps = $scope.getEnabledSteps(); + //setting variable equal to step you were on when next() was invoked + var index = stepIdx($scope.selectedStep); + //checking to see if callback is a function + if(angular.isFunction(callback)){ + if(callback()){ + if (index === enabledSteps.length - 1) { + this.finish(); + } else { + //invoking goTo() with step number next in line + $scope.goTo(enabledSteps[index + 1]); + } + } else { + return; + } + } + if (!callback) { + //completed property set on scope which is used to add class/remove class from progress bar $scope.selectedStep.completed = true; } - if (index === $scope.steps.length - 1) { + //checking to see if this is the last step. If it is next behaves the same as finish() + if (index === enabledSteps.length - 1) { this.finish(); } else { - $scope.goTo($scope.steps[index + 1]); + //invoking goTo() with step number next in line + $scope.goTo(enabledSteps[index + 1]); } - } - + + }; + + //used to traverse to any step, step number placed as argument this.goTo = function(step) { - var stepTo; - if (_.isNumber(step)) { - stepTo = $scope.steps[step]; - } else { - stepTo = _.find($scope.steps, {title: step}); - } - $scope.goTo(stepTo); - } - + //wrapped inside $timeout so newly enabled steps are included. + $timeout(function() { + var enabledSteps = $scope.getEnabledSteps(); + var stepTo; + //checking that step is a Number + if (angular.isNumber(step)) { + stepTo = enabledSteps[step]; + } else { + //finding the step associated with the title entered as goTo argument + stepTo = stepByTitle(step); + } + //going to step + $scope.goTo(stepTo); + }); + }; + + //calls finish() which calls onFinish() which is declared on an attribute and linked to controller via wizard directive. this.finish = function() { - $scope.onFinish && $scope.onFinish(); - } - - this.cancel = this.previous = function() { - var index = _.indexOf($scope.steps , $scope.selectedStep); + if ($scope.onFinish) { + $scope.onFinish(); + } + }; + + this.previous = function() { + //getting index of current step + var index = stepIdx($scope.selectedStep); + //ensuring you aren't trying to go back from the first step if (index === 0) { throw new Error("Can't go back. It's already in step 0"); } else { - $scope.goTo($scope.steps[index - 1]); + //go back one step from current step + $scope.goTo($scope.getEnabledSteps()[index - 1]); } - } - - + }; + + //cancel is alias for previous. + this.cancel = function() { + if ($scope.onCancel) { + //onCancel is linked to controller via wizard directive: + $scope.onCancel(); + } else { + //getting index of current step + var index = stepIdx($scope.selectedStep); + //ensuring you aren't trying to go back from the first step + if (index === 0) { + throw new Error("Can't go back. It's already in step 0"); + } else { + //go back one step from current step + $scope.goTo($scope.getEnabledSteps()[0]); + } + } + }; + + //reset + this.reset = function(){ + //traverse steps array and set each "completed" property to false + angular.forEach($scope.getEnabledSteps(), function (step) { + step.completed = false; + }); + //go to first step + this.goTo(0); + }; + + //change edit mode + this.setEditMode = function(mode) { + $scope.editMode = mode; + handleEditModeChange(); + }; }] - } + }; }); + function wizardButtonDirective(action) { angular.module('mgo-angular-wizard') - .directive(action, ['$parse', function($parse) { + .directive(action, function() { return { restrict: 'A', replace: false, require: '^wizard', link: function($scope, $element, $attrs, wizard) { - + $element.on("click", function(e) { e.preventDefault(); $scope.$apply(function() { - var fn = $parse($attrs[action]); - fn && fn(); + $scope.$eval($attrs[action]); wizard[action.replace("wz", "").toLowerCase()](); }); }); } - } - }]); + }; + }); } wizardButtonDirective('wzNext'); wizardButtonDirective('wzPrevious'); wizardButtonDirective('wzFinish'); wizardButtonDirective('wzCancel'); - - +wizardButtonDirective('wzReset'); + angular.module('mgo-angular-wizard').factory('WizardHandler', function() { var service = {}; @@ -181,11 +489,11 @@ angular.module('mgo-angular-wizard').factory('WizardHandler', function() { service.addWizard = function(name, wizard) { wizards[name] = wizard; - } + }; - service.deleteWizard = function(name) { + service.removeWizard = function(name) { delete wizards[name]; - } + }; service.wizard = function(name) { var nameToUse = name; @@ -194,7 +502,7 @@ angular.module('mgo-angular-wizard').factory('WizardHandler', function() { } return wizards[nameToUse]; - } + }; return service; -}); \ No newline at end of file +}); diff --git a/dist/angular-wizard.less b/dist/angular-wizard.less index 6b2306f..5f13666 100644 --- a/dist/angular-wizard.less +++ b/dist/angular-wizard.less @@ -1,24 +1,24 @@ +@wz-color-default: #E6E6E6; +@wz-color-current: #808080; +@wz-color-done: #339933; +@wz-color-editing: #FF0000; + .steps-indicator { /* ---- steps quantity ---- */ - @color-default: #E6E6E6; - @color-current: #808080; - @color-done: #339933; - @color-editing: #FF0000; - position: absolute; right: 0; bottom: 0; left: 0; margin: 0; - padding: 0; + padding: 20px 0 0 0; height: 30px; list-style: none; &:before { - background-color: @color-default; + background-color: @wz-color-default; content: ''; position: absolute; height: 1px; @@ -87,7 +87,7 @@ line-height: 15px; a { - color: @color-current; + color: @wz-color-current; text-decoration: none; text-transform: uppercase; font-weight: bold; @@ -101,13 +101,13 @@ width: 14px; height: 14px; border-radius: 100%; - background-color: @color-default; + background-color: @wz-color-default; content: ''; transition: 0.25s; } &:hover { - color: darken(@color-current, 20%); + color: darken(@wz-color-current, 20%); } } } @@ -156,7 +156,7 @@ pointer-events: none; a:hover { - color: @color-current; + color: @wz-color-current; } } @@ -166,14 +166,14 @@ } li.current a:before { - background-color: @color-current; + background-color: @wz-color-current; } li.done a:before { - background-color: @color-done; + background-color: @wz-color-done; } li.editing a:before { - background-color: @color-editing; + background-color: @wz-color-editing; } -} \ No newline at end of file +} diff --git a/dist/angular-wizard.min.css b/dist/angular-wizard.min.css index 7b055fe..32cbfb4 100644 --- a/dist/angular-wizard.min.css +++ b/dist/angular-wizard.min.css @@ -1 +1 @@ -.steps-indicator{position:absolute;right:0;bottom:0;left:0;height:30px;padding:0;margin:0;list-style:none}.steps-indicator:before{position:absolute;height:1px;background-color:#e6e6e6;content:''}.steps-indicator.steps-2:before{right:calc(25%);left:calc(25%)}.steps-indicator.steps-3:before{right:calc(16.666666666666668%);left:calc(16.666666666666668%)}.steps-indicator.steps-4:before{right:calc(12.5%);left:calc(12.5%)}.steps-indicator.steps-5:before{right:calc(10%);left:calc(10%)}.steps-indicator.steps-6:before{right:calc(8.333333333333334%);left:calc(8.333333333333334%)}.steps-indicator.steps-7:before{right:calc(7.142857142857143%);left:calc(7.142857142857143%)}.steps-indicator.steps-8:before{right:calc(6.25%);left:calc(6.25%)}.steps-indicator.steps-9:before{right:calc(5.555555555555555%);left:calc(5.555555555555555%)}.steps-indicator.steps-10:before{right:calc(5%);left:calc(5%)}.steps-indicator *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.steps-indicator li{position:relative;float:left;padding:0;padding-top:10px;margin:0;line-height:15px;text-align:center}.steps-indicator li a{font-weight:bold;color:#808080;text-decoration:none;text-transform:uppercase;cursor:pointer;transition:.25s}.steps-indicator li a:before{position:absolute;top:-7px;left:calc(43%);width:14px;height:14px;background-color:#e6e6e6;border-radius:100%;content:'';transition:.25s}.steps-indicator li a:hover{color:#4d4d4d}.steps-indicator.steps-2 li{width:calc(50%)}.steps-indicator.steps-3 li{width:calc(33.333333333333336%)}.steps-indicator.steps-4 li{width:calc(25%)}.steps-indicator.steps-5 li{width:calc(20%)}.steps-indicator.steps-6 li{width:calc(16.666666666666668%)}.steps-indicator.steps-7 li{width:calc(14.285714285714286%)}.steps-indicator.steps-8 li{width:calc(12.5%)}.steps-indicator.steps-9 li{width:calc(11.11111111111111%)}.steps-indicator.steps-10 li{width:calc(10%)}.steps-indicator.steps-11 li{width:calc(9.090909090909092%)}.steps-indicator li.default{pointer-events:none}.steps-indicator li.default a:hover{color:#808080}.steps-indicator li.current,.steps-indicator li.editing{pointer-events:none}.steps-indicator li.current a:before{background-color:#808080}.steps-indicator li.done a:before{background-color:#393}.steps-indicator li.editing a:before{background-color:#f00} \ No newline at end of file +.steps-indicator li a:before,.steps-indicator:before{background-color:#e6e6e6;content:''}.steps-indicator li.current,.steps-indicator li.default,.steps-indicator li.editing{pointer-events:none}.steps-indicator{right:0;bottom:0;left:0;margin:0;padding:20px 0 0;height:30px;list-style:none}.steps-indicator:before{position:absolute;height:1px}.steps-indicator.steps-2:before{left:calc(100% / 2 / 2);right:calc(100% / 2 / 2)}.steps-indicator.steps-3:before{left:calc(100% / 3 / 2);right:calc(100% / 3 / 2)}.steps-indicator.steps-4:before{left:calc(100% / 4 / 2);right:calc(100% / 4 / 2)}.steps-indicator.steps-5:before{left:calc(100% / 5 / 2);right:calc(100% / 5 / 2)}.steps-indicator.steps-6:before{left:calc(100% / 6 / 2);right:calc(100% / 6 / 2)}.steps-indicator.steps-7:before{left:calc(100% / 7 / 2);right:calc(100% / 7 / 2)}.steps-indicator.steps-8:before{left:calc(100% / 8 / 2);right:calc(100% / 8 / 2)}.steps-indicator.steps-9:before{left:calc(100% / 9 / 2);right:calc(100% / 9 / 2)}.steps-indicator.steps-10:before{left:calc(100% / 10 / 2);right:calc(100% / 10 / 2)}.steps-indicator *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.steps-indicator li{position:relative;float:left;margin:0;padding:0;padding-top:10px;text-align:center;line-height:15px}.steps-indicator li a{color:grey;text-decoration:none;text-transform:uppercase;font-weight:700;transition:.25s;cursor:pointer}.steps-indicator li a:before{position:absolute;top:-7px;left:calc(50% - 7px);width:14px;height:14px;border-radius:100%;transition:.25s}.steps-indicator li a:hover{color:#4d4d4d}.steps-indicator.steps-2 li{width:calc(100% / 2)}.steps-indicator.steps-3 li{width:calc(100% / 3)}.steps-indicator.steps-4 li{width:calc(100% / 4)}.steps-indicator.steps-5 li{width:calc(100% / 5)}.steps-indicator.steps-6 li{width:calc(100% / 6)}.steps-indicator.steps-7 li{width:calc(100% / 7)}.steps-indicator.steps-8 li{width:calc(100% / 8)}.steps-indicator.steps-9 li{width:calc(100% / 9)}.steps-indicator.steps-10 li{width:calc(100% / 10)}.steps-indicator.steps-11 li{width:calc(100% / 11)}.steps-indicator li.default a:hover{color:grey}.steps-indicator li.current a:before{background-color:grey}.steps-indicator li.done a:before{background-color:#393}.steps-indicator li.editing a:before{background-color:red} \ No newline at end of file diff --git a/dist/angular-wizard.min.js b/dist/angular-wizard.min.js index 4082019..e3c7e7e 100644 --- a/dist/angular-wizard.min.js +++ b/dist/angular-wizard.min.js @@ -1,7 +1,7 @@ /** - * Easy to use Wizard library for AngularJS - * @version v0.2.0 - 2014-01-30 * @link https://github.com/mgonto/angular-wizard + * Easy to use Wizard library for Angular JS + * @version v1.1.1 - 2017-06-07 * @link https://github.com/mgonto/angular-wizard * @author Martin Gontovnikas * @license MIT License, http://www.opensource.org/licenses/MIT */ -function wizardButtonDirective(a){angular.module("mgo-angular-wizard").directive(a,["$parse",function(b){return{restrict:"A",replace:!1,require:"^wizard",link:function(c,d,e,f){d.on("click",function(d){d.preventDefault(),c.$apply(function(){var c=b(e[a]);c&&c(),f[a.replace("wz","").toLowerCase()]()})})}}}])}angular.module("templates-angularwizard",["step.html","wizard.html"]),angular.module("step.html",[]).run(["$templateCache",function(a){a.put("step.html",'
\n
')}]),angular.module("wizard.html",[]).run(["$templateCache",function(a){a.put("wizard.html",'
\n
\n \n
')}]),angular.module("mgo-angular-wizard",["templates-angularwizard"]),angular.module("mgo-angular-wizard").directive("step",function(){return{restrict:"E",replace:!0,transclude:!0,scope:{title:"@"},require:"^wizard",templateUrl:"step.html",link:function(a,b,c,d){d.addStep(a)}}}),angular.module("mgo-angular-wizard").directive("wizard",function(){return{restrict:"E",replace:!0,transclude:!0,scope:{currentStep:"=",onFinish:"&",hideIndicators:"=",editMode:"=",name:"@"},templateUrl:"wizard.html",controller:["$scope","$element","WizardHandler",function(a,b,c){function d(){_.each(a.steps,function(a){a.selected=!1}),a.selectedStep=null}c.addWizard(a.name||c.defaultName,this),a.$on("$destroy",function(){c.removeWizard(a.name||c.defaultName)}),a.steps=[],a.$watch("currentStep",function(b){b&&a.selectedStep&&a.selectedStep.title!==a.currentStep&&a.goTo(_.find(a.steps,{title:a.currentStep}))}),a.$watch("[editMode, steps.length]",function(){var b=a.editMode;_.isUndefined(b)||_.isNull(b)||b&&_.each(a.steps,function(a){a.completed=!0})},!0),this.addStep=function(b){a.steps.push(b),1===a.steps.length&&a.goTo(a.steps[0])},a.goTo=function(b){d(),a.selectedStep=b,a.currentStep&&(a.currentStep=b.title),b.selected=!0},this.next=function(b){var c=_.indexOf(a.steps,a.selectedStep);b||(a.selectedStep.completed=!0),c===a.steps.length-1?this.finish():a.goTo(a.steps[c+1])},this.goTo=function(b){var c;c=_.isNumber(b)?a.steps[b]:_.find(a.steps,{title:b}),a.goTo(c)},this.finish=function(){a.onFinish&&a.onFinish()},this.cancel=this.previous=function(){var b=_.indexOf(a.steps,a.selectedStep);if(0===b)throw new Error("Can't go back. It's already in step 0");a.goTo(a.steps[b-1])}}]}}),wizardButtonDirective("wzNext"),wizardButtonDirective("wzPrevious"),wizardButtonDirective("wzFinish"),wizardButtonDirective("wzCancel"),angular.module("mgo-angular-wizard").factory("WizardHandler",function(){var a={},b={};return a.defaultName="defaultWizard",a.addWizard=function(a,c){b[a]=c},a.deleteWizard=function(a){delete b[a]},a.wizard=function(c){var d=c;return c||(d=a.defaultName),b[d]},a}); \ No newline at end of file +function wizardButtonDirective(a){angular.module("mgo-angular-wizard").directive(a,function(){return{restrict:"A",replace:!1,require:"^wizard",link:function(b,c,d,e){c.on("click",function(c){c.preventDefault(),b.$apply(function(){b.$eval(d[a]),e[a.replace("wz","").toLowerCase()]()})})}}})}angular.module("templates-angularwizard",["step.html","wizard.html"]),angular.module("step.html",[]).run(["$templateCache",function(a){a.put("step.html",'
\n
')}]),angular.module("wizard.html",[]).run(["$templateCache",function(a){a.put("wizard.html",'
\n

{{ selectedStep.wzHeadingTitle }}

\n\n
\n \n
\n
\n')}]),angular.module("mgo-angular-wizard",["templates-angularwizard"]),angular.module("mgo-angular-wizard").directive("wzStep",function(){return{restrict:"EA",replace:!0,transclude:!0,scope:{wzTitle:"@",wzHeadingTitle:"@",canenter:"=",canexit:"=",disabled:"@?wzDisabled",description:"@",wzData:"=",wzOrder:"@?"},require:"^wizard",templateUrl:function(a,b){return b.template||"step.html"},link:function(a,b,c,d){c.$observe("wzTitle",function(b){a.title=a.wzTitle}),a.title=a.wzTitle,d.addStep(a),a.$on("$destroy",function(){d.removeStep(a)})}}}),angular.module("mgo-angular-wizard").directive("wizard",function(){return{restrict:"EA",replace:!0,transclude:!0,scope:{currentStep:"=",onCancel:"&",onFinish:"&",hideIndicators:"=",editMode:"=",name:"@",indicatorsPosition:"@?"},templateUrl:function(a,b){return b.template||"wizard.html"},controller:["$scope","$element","$log","WizardHandler","$q","$timeout",function($scope,$element,$log,WizardHandler,$q,$timeout){function canEnterStep(step){var defer,canEnter;if(void 0===step.canenter)return!0;if("boolean"==typeof step.canenter)return step.canenter;if("string"==typeof step.canenter){var splitFunction=step.canenter.split("(");canEnter=eval("$scope.$parent."+splitFunction[0]+"($scope.context"+splitFunction[1])}else canEnter=step.canenter($scope.context);return angular.isFunction(canEnter.then)?(defer=$q.defer(),canEnter.then(function(a){defer.resolve(a)}),defer.promise):canEnter===!0}function canExitStep(step,stepTo){var defer,canExit;if("undefined"==typeof step.canexit||$scope.getStepNumber(stepTo)<$scope.currentStepNumber())return!0;if("boolean"==typeof step.canexit)return step.canexit;if("string"==typeof step.canexit){var splitFunction=step.canexit.split("(");canExit=eval("$scope.$parent."+splitFunction[0]+"($scope.context"+splitFunction[1])}else canExit=step.canexit($scope.context);return angular.isFunction(canExit.then)?(defer=$q.defer(),canExit.then(function(a){defer.resolve(a)}),defer.promise):canExit===!0}function unselectAll(){angular.forEach($scope.getEnabledSteps(),function(a){a.selected=!1}),$scope.selectedStep=null}void 0==$scope.indicatorsPosition&&($scope.indicatorsPosition="bottom");var firstRun=!0;WizardHandler.addWizard($scope.name||WizardHandler.defaultName,this),$scope.$on("$destroy",function(){WizardHandler.removeWizard($scope.name||WizardHandler.defaultName)}),$scope.steps=[];var stepIdx=function(a){var b=0,c=-1;return angular.forEach($scope.getEnabledSteps(),function(d){d===a&&(c=b),b++}),c},stepByTitle=function(a){var b=null;return angular.forEach($scope.getEnabledSteps(),function(c){c.wzTitle===a&&(b=c)}),b},handleEditModeChange=function(){var a=$scope.editMode;if(!angular.isUndefined(a)&&null!==a&&(angular.forEach($scope.steps,function(b){b.completed=a}),!a)){var b=$scope.currentStepNumber()-1;angular.forEach($scope.getEnabledSteps(),function(a,c){b>c&&(a.completed=!0)})}};$scope.context={},$scope.$watch("currentStep",function(a){if(a){var b=$scope.selectedStep.wzTitle;$scope.selectedStep&&b!==$scope.currentStep&&$scope.goTo(stepByTitle($scope.currentStep))}}),$scope.$watch("[editMode, steps.length]",function(){handleEditModeChange()},!0),this.addStep=function(a){var b=a.wzOrder>=0&&!$scope.steps[a.wzOrder]?a.wzOrder:$scope.steps.length;$scope.steps[b]=a,$scope.getEnabledSteps()[0]===a&&$scope.goTo($scope.getEnabledSteps()[0])},this.removeStep=function(a){var b=$scope.steps.indexOf(a);b>0&&$scope.steps.splice(b,1)},this.context=$scope.context,$scope.getStepNumber=function(a){return stepIdx(a)+1},$scope.goTo=function(a){if(firstRun)unselectAll(),$scope.selectedStep=a,angular.isUndefined($scope.currentStep)||($scope.currentStep=a.wzTitle),a.selected=!0,$scope.$emit("wizard:stepChanged",{step:a,index:stepIdx(a)}),firstRun=!1;else{var b;$scope.currentStepNumber()>0?b=$scope.currentStepNumber()-1:0===$scope.currentStepNumber()&&(b=0),$q.all([canExitStep($scope.getEnabledSteps()[b],a),canEnterStep(a)]).then(function(b){b[0]&&b[1]?(unselectAll(),$scope.selectedStep=a,angular.isUndefined($scope.currentStep)||($scope.currentStep=a.wzTitle),a.selected=!0,$scope.$emit("wizard:stepChanged",{step:a,index:stepIdx(a)})):$scope.$emit("wizard:stepChangeFailed",{step:a,index:_.indexOf($scope.getEnabledSteps(),a)})})}},$scope.currentStepNumber=function(){return stepIdx($scope.selectedStep)+1},$scope.getEnabledSteps=function(){return $scope.steps.filter(function(a){return a&&"true"!==a.disabled})},this.currentStepTitle=function(){return $scope.selectedStep.wzTitle},this.currentStepDescription=function(){return $scope.selectedStep.description},this.currentStep=function(){return $scope.selectedStep},this.totalStepCount=function(){return $scope.getEnabledSteps().length},this.getEnabledSteps=function(){return $scope.getEnabledSteps()},this.currentStepNumber=function(){return $scope.currentStepNumber()},this.next=function(a){var b=$scope.getEnabledSteps(),c=stepIdx($scope.selectedStep);if(angular.isFunction(a)){if(!a())return;c===b.length-1?this.finish():$scope.goTo(b[c+1])}a||($scope.selectedStep.completed=!0),c===b.length-1?this.finish():$scope.goTo(b[c+1])},this.goTo=function(a){$timeout(function(){var b,c=$scope.getEnabledSteps();b=angular.isNumber(a)?c[a]:stepByTitle(a),$scope.goTo(b)})},this.finish=function(){$scope.onFinish&&$scope.onFinish()},this.previous=function(){var a=stepIdx($scope.selectedStep);if(0===a)throw new Error("Can't go back. It's already in step 0");$scope.goTo($scope.getEnabledSteps()[a-1])},this.cancel=function(){if($scope.onCancel)$scope.onCancel();else{var a=stepIdx($scope.selectedStep);if(0===a)throw new Error("Can't go back. It's already in step 0");$scope.goTo($scope.getEnabledSteps()[0])}},this.reset=function(){angular.forEach($scope.getEnabledSteps(),function(a){a.completed=!1}),this.goTo(0)},this.setEditMode=function(a){$scope.editMode=a,handleEditModeChange()}}]}}),wizardButtonDirective("wzNext"),wizardButtonDirective("wzPrevious"),wizardButtonDirective("wzFinish"),wizardButtonDirective("wzCancel"),wizardButtonDirective("wzReset"),angular.module("mgo-angular-wizard").factory("WizardHandler",function(){var a={},b={};return a.defaultName="defaultWizard",a.addWizard=function(a,c){b[a]=c},a.removeWizard=function(a){delete b[a]},a.wizard=function(c){var d=c;return c||(d=a.defaultName),b[d]},a}); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index 3a1f1a5..1ab4bb1 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -2,7 +2,7 @@ // Generated on Fri Aug 09 2013 14:14:35 GMT-0500 (CDT) module.exports = function(config) { - config.set({ + var configuration = { // base path, that will be used to resolve files and exclude basePath: '', @@ -11,25 +11,39 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - 'http://code.angularjs.org/1.1.4/angular.js', - 'http://code.angularjs.org/1.1.4/angular-resource.js', - 'http://code.angularjs.org/1.1.4/angular-mocks.js', - 'http://cdnjs.cloudflare.com/ajax/libs/lodash.js/1.2.0/lodash.min.js', - 'dist/*.js', + 'https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.js', + 'https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular-mocks.js', + 'http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js', + 'src/*.js', 'test/*.js' ], - - // list of files to exclude - exclude: [ - + plugins: [ + "karma-phantomjs-launcher", + "karma-jasmine", + "karma-coverage" ], + preprocessors: { + "src/*.js": ["coverage"] + }, + + // optionally, configure the reporter + coverageReporter: { + dir : "coverage/", + reporters: [ + {type: "lcov", subdir: "lcov"} + ] + }, // test results reporter to use - // possible values: 'dots', 'progress', 'junit' - reporters: ['progress'], + // possible values: "dots", "progress", "junit" + reporters: ["dots", "coverage"], + // list of files to exclude + exclude: [ + + ], // web server port port: 9876, @@ -71,5 +85,7 @@ module.exports = function(config) { // if true, it capture browsers, run tests and exit singleRun: false - }); + }; + + config.set(configuration); }; diff --git a/karma.underscore.conf.js b/karma.underscore.conf.js deleted file mode 100644 index ab1d824..0000000 --- a/karma.underscore.conf.js +++ /dev/null @@ -1,75 +0,0 @@ -// Karma configuration -// Generated on Fri Aug 09 2013 14:14:35 GMT-0500 (CDT) - -module.exports = function(config) { - config.set({ - - // base path, that will be used to resolve files and exclude - basePath: '', - - frameworks: ["jasmine"], - - // list of files / patterns to load in the browser - files: [ - 'http://code.angularjs.org/1.1.4/angular.js', - 'http://code.angularjs.org/1.1.4/angular-resource.js', - 'http://code.angularjs.org/1.1.4/angular-mocks.js', - 'http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js', - 'src/restangular.js', - 'test/*.js' - ], - - - // list of files to exclude - exclude: [ - - ], - - - // test results reporter to use - // possible values: 'dots', 'progress', 'junit' - reporters: ['progress'], - - - // web server port - port: 9877, - - - // cli runner port - runnerPort: 9101, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera - // - Safari (only Mac) - // - PhantomJS - // - IE (only Windows) - browsers: ['PhantomJS'], - - - // If browser does not capture in given timeout [ms], kill it - captureTimeout: 60000, - - - // Continuous Integration mode - // if true, it capture browsers, run tests and exit - singleRun: false - - }); -}; diff --git a/package.json b/package.json index eae9ced..c64fae3 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "angular-wizard", - "description": "Easy to use Wizard library for AngularJS", - "version": "0.2.0", + "description": "Easy to use Wizard library for Angular JS", + "version": "1.1.1", "filename": "angular-wizard.min.js", - "main": "./dist/angular-wizard.min.js", + "main": "./dist/angular-wizard.js", "homepage": "https://github.com/mgonto/angular-wizard", "author": "Martin Gontovnikas ", "repository": { @@ -13,6 +13,9 @@ "engines": { "node": ">= 0.9" }, + "scripts": { + "test": "grunt travis --verbose" + }, "keywords": [ "angular", "client", @@ -29,23 +32,31 @@ ], "dependencies": {}, "devDependencies": { + "codacy-coverage": "*", + "coveralls": "*", + "grunt": "~0.4.1", "grunt-bower": "*", "grunt-bower-task": "*", - "grunt-zip": "*", - "grunt": "~0.4.1", - "grunt-recess": "~0.3.0", + "grunt-bump": "0.0.6", + "grunt-cli": "^0.1.x", "grunt-contrib-clean": "~0.4.1", - "grunt-contrib-copy": "~0.4.1", "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-cssmin": "~0.12.2", + "grunt-contrib-less": "~1.0.0", "grunt-contrib-uglify": "~0.2.0", - "grunt-karma": "~0.5.0", - "grunt-ngmin": "0.0.2", - "grunt-html2js": "~0.1.3", "grunt-conventional-changelog": "~0.1.1", - "grunt-bump": "0.0.6" - }, - "scripts": { - "test": "grunt travis --verbose" + "grunt-html2js": "~0.1.3", + "grunt-karma": "~0.9.x", + "karma": "^0.12.0", + "karma-coverage": "^1.1.1", + "grunt-ng-annotate": "^0.6.0", + "grunt-zip": "*", + "jasmine-core": "^2.1.3", + "karma-chrome-launcher": "^0.1.7", + "karma-firefox-launcher": "~0.1.x", + "karma-jasmine": "~0.3.x", + "karma-phantomjs-launcher": "~1.0.x" }, "license": "MIT" } \ No newline at end of file diff --git a/src/angular-wizard.less b/src/angular-wizard.less index 6b2306f..5f13666 100644 --- a/src/angular-wizard.less +++ b/src/angular-wizard.less @@ -1,24 +1,24 @@ +@wz-color-default: #E6E6E6; +@wz-color-current: #808080; +@wz-color-done: #339933; +@wz-color-editing: #FF0000; + .steps-indicator { /* ---- steps quantity ---- */ - @color-default: #E6E6E6; - @color-current: #808080; - @color-done: #339933; - @color-editing: #FF0000; - position: absolute; right: 0; bottom: 0; left: 0; margin: 0; - padding: 0; + padding: 20px 0 0 0; height: 30px; list-style: none; &:before { - background-color: @color-default; + background-color: @wz-color-default; content: ''; position: absolute; height: 1px; @@ -87,7 +87,7 @@ line-height: 15px; a { - color: @color-current; + color: @wz-color-current; text-decoration: none; text-transform: uppercase; font-weight: bold; @@ -101,13 +101,13 @@ width: 14px; height: 14px; border-radius: 100%; - background-color: @color-default; + background-color: @wz-color-default; content: ''; transition: 0.25s; } &:hover { - color: darken(@color-current, 20%); + color: darken(@wz-color-current, 20%); } } } @@ -156,7 +156,7 @@ pointer-events: none; a:hover { - color: @color-current; + color: @wz-color-current; } } @@ -166,14 +166,14 @@ } li.current a:before { - background-color: @color-current; + background-color: @wz-color-current; } li.done a:before { - background-color: @color-done; + background-color: @wz-color-done; } li.editing a:before { - background-color: @color-editing; + background-color: @wz-color-editing; } -} \ No newline at end of file +} diff --git a/src/angularModule.js b/src/angularModule.js index 81c64dc..816d5be 100644 --- a/src/angularModule.js +++ b/src/angularModule.js @@ -1 +1 @@ -angular.module('mgo-angular-wizard', ['templates-angularwizard']); \ No newline at end of file +angular.module('mgo-angular-wizard', ['templates-angularwizard']); diff --git a/src/step.html b/src/step.html index 49148d9..59f13f2 100644 --- a/src/step.html +++ b/src/step.html @@ -1,2 +1,2 @@ -
+
\ No newline at end of file diff --git a/src/step.js b/src/step.js index 5b2273c..587bbe1 100644 --- a/src/step.js +++ b/src/step.js @@ -1,15 +1,31 @@ -angular.module('mgo-angular-wizard').directive('step', function() { +angular.module('mgo-angular-wizard').directive('wzStep', function() { return { - restrict: 'E', + restrict: 'EA', replace: true, transclude: true, scope: { - title: '@' + wzTitle: '@', + wzHeadingTitle: '@', + canenter : '=', + canexit : '=', + disabled: '@?wzDisabled', + description: '@', + wzData: '=', + wzOrder: '@?' }, require: '^wizard', - templateUrl: 'step.html', - link: function($scope, $element, $attrs, wizard) { + templateUrl: function(element, attributes) { + return attributes.template || "step.html"; + }, + link: function ($scope, $element, $attrs, wizard) { + $attrs.$observe('wzTitle', function (value) { + $scope.title = $scope.wzTitle; + }); + $scope.title = $scope.wzTitle; wizard.addStep($scope); + $scope.$on('$destroy', function(){ + wizard.removeStep($scope); + }); } - } -}); \ No newline at end of file + }; +}); diff --git a/src/wizard.html b/src/wizard.html index 9c521e8..7870906 100644 --- a/src/wizard.html +++ b/src/wizard.html @@ -1,8 +1,11 @@
-
-
\ No newline at end of file +
+ diff --git a/src/wizard.js b/src/wizard.js index c2ae691..d0c0b94 100644 --- a/src/wizard.js +++ b/src/wizard.js @@ -1,104 +1,390 @@ +//wizard directive angular.module('mgo-angular-wizard').directive('wizard', function() { return { - restrict: 'E', + restrict: 'EA', replace: true, transclude: true, scope: { currentStep: '=', + onCancel: '&', onFinish: '&', hideIndicators: '=', editMode: '=', - name: '@' + name: '@', + indicatorsPosition: '@?' }, - templateUrl: 'wizard.html', - controller: ['$scope', '$element', 'WizardHandler', function($scope, $element, WizardHandler) { - + templateUrl: function(element, attributes) { + return attributes.template || "wizard.html"; + }, + + //controller for wizard directive, treat this just like an angular controller + controller: ['$scope', '$element', '$log', 'WizardHandler', '$q', '$timeout', function ($scope, $element, $log, WizardHandler, $q, $timeout) { + //setting default step position if none declared. + if ($scope.indicatorsPosition == undefined) { + $scope.indicatorsPosition = 'bottom'; + } + //this variable allows directive to load without having to pass any step validation + var firstRun = true; + //creating instance of wizard, passing this as second argument allows access to functions attached to this via Service WizardHandler.addWizard($scope.name || WizardHandler.defaultName, this); + $scope.$on('$destroy', function() { WizardHandler.removeWizard($scope.name || WizardHandler.defaultName); }); - + + //steps array where all the scopes of each step are added $scope.steps = []; - + + var stepIdx = function(step) { + var idx = 0; + var res = -1; + angular.forEach($scope.getEnabledSteps(), function(currStep) { + if (currStep === step) { + res = idx; + } + idx++; + }); + return res; + }; + + var stepByTitle = function(titleToFind) { + var foundStep = null; + angular.forEach($scope.getEnabledSteps(), function(step) { + if (step.wzTitle === titleToFind) { + foundStep = step; + } + }); + return foundStep; + }; + + + //update completed state for each step based on the editMode and current step number + var handleEditModeChange = function() { + var editMode = $scope.editMode; + if (angular.isUndefined(editMode) || (editMode === null)) return; + + //Set completed for all steps to the value of editMode + angular.forEach($scope.steps, function (step) { + step.completed = editMode; + }); + + //If editMode is false, set ONLY ENABLED steps with index lower then completedIndex to completed + if (!editMode) { + var completedStepsIndex = $scope.currentStepNumber() - 1; + angular.forEach($scope.getEnabledSteps(), function(step, stepIndex) { + if(stepIndex < completedStepsIndex) { + step.completed = true; + } + }); + } + }; + + //access to context object for step validation + $scope.context = {}; + + //watching changes to currentStep $scope.$watch('currentStep', function(step) { + //checking to make sure currentStep is truthy value if (!step) return; - - if ($scope.selectedStep && $scope.selectedStep.title !== $scope.currentStep) { - $scope.goTo(_.find($scope.steps, {title: $scope.currentStep})); + //setting stepTitle equal to current step title or default title + var stepTitle = $scope.selectedStep.wzTitle; + if ($scope.selectedStep && stepTitle !== $scope.currentStep) { + //invoking goTo() with step title as argument + $scope.goTo(stepByTitle($scope.currentStep)); } - }); - + + //watching steps array length and editMode value, if edit module is undefined or null the nothing is done + //if edit mode is truthy, then all steps are marked as completed $scope.$watch('[editMode, steps.length]', function() { - var editMode = $scope.editMode; - if (_.isUndefined(editMode) || _.isNull(editMode)) return; - - if (editMode) { - _.each($scope.steps, function(step) { - step.completed = true; - }); - } + handleEditModeChange(); }, true); - + + //called each time step directive is loaded this.addStep = function(step) { - $scope.steps.push(step); - if ($scope.steps.length === 1) { - $scope.goTo($scope.steps[0]); + var wzOrder = (step.wzOrder >= 0 && !$scope.steps[step.wzOrder]) ? step.wzOrder : $scope.steps.length; + //adding the scope of directive onto step array + $scope.steps[wzOrder] = step; + //if this step is the new first then goTo it + if ($scope.getEnabledSteps()[0] === step) { + //goTo first step + $scope.goTo($scope.getEnabledSteps()[0]); } - } - + }; + + //called each time step directive is destroyed + this.removeStep = function (step) { + var index = $scope.steps.indexOf(step); + if (index > 0) { + $scope.steps.splice(index, 1); + } + }; + + this.context = $scope.context; + + $scope.getStepNumber = function(step) { + return stepIdx(step) + 1; + }; + $scope.goTo = function(step) { - unselectAll(); - $scope.selectedStep = step; - if ($scope.currentStep) { - $scope.currentStep = step.title; + //if this is the first time the wizard is loading it bi-passes step validation + if(firstRun){ + //deselect all steps so you can set fresh below + unselectAll(); + $scope.selectedStep = step; + //making sure current step is not undefined + if (!angular.isUndefined($scope.currentStep)) { + $scope.currentStep = step.wzTitle; + } + //setting selected step to argument passed into goTo() + step.selected = true; + //emit event upwards with data on goTo() invoktion + $scope.$emit('wizard:stepChanged', {step: step, index: stepIdx(step)}); + //setting variable to false so all other step changes must pass validation + firstRun = false; + } else { + //createing variables to capture current state that goTo() was invoked from and allow booleans + var thisStep; + //getting data for step you are transitioning out of + if($scope.currentStepNumber() > 0){ + thisStep = $scope.currentStepNumber() - 1; + } else if ($scope.currentStepNumber() === 0){ + thisStep = 0; + } + //$log.log('steps[thisStep] Data: ', $scope.getEnabledSteps()[thisStep].canexit); + $q.all([canExitStep($scope.getEnabledSteps()[thisStep], step), canEnterStep(step)]).then(function(data) { + if(data[0] && data[1]){ + //deselect all steps so you can set fresh below + unselectAll(); + + //$log.log('value for canExit argument: ', $scope.currentStep.canexit); + $scope.selectedStep = step; + //making sure current step is not undefined + if(!angular.isUndefined($scope.currentStep)){ + $scope.currentStep = step.wzTitle; + } + //setting selected step to argument passed into goTo() + step.selected = true; + //emit event upwards with data on goTo() invoktion + $scope.$emit('wizard:stepChanged', {step: step, index: stepIdx(step)}); + //$log.log('current step number: ', $scope.currentStepNumber()); + } else { + $scope.$emit('wizard:stepChangeFailed', {step: step, index: _.indexOf($scope.getEnabledSteps(), step)}); + } + }); + } + }; + + function canEnterStep(step) { + var defer; + var canEnter; + //If no validation function is provided, allow the user to enter the step + if(step.canenter === undefined){ + return true; + } + //If canenter is a boolean value instead of a function, return the value + if(typeof step.canenter === 'boolean'){ + return step.canenter; + } + //If canenter is a string instead of a function, evaluate the function + if(typeof step.canenter === 'string'){ + var splitFunction = step.canenter.split('('); + canEnter = eval('$scope.$parent.' + splitFunction[0] + '($scope.context' + splitFunction[1]) + } else { + canEnter = step.canenter($scope.context); + } + //Check to see if the canenter function is a promise which needs to be returned + if(angular.isFunction(canEnter.then)){ + defer = $q.defer(); + canEnter.then(function(response){ + defer.resolve(response); + }); + return defer.promise; + } else { + return canEnter === true; } - step.selected = true; } - + + function canExitStep(step, stepTo) { + var defer; + var canExit; + //Exiting the step should be allowed if no validation function was provided or if the user is moving backwards + if(typeof(step.canexit) === 'undefined' || $scope.getStepNumber(stepTo) < $scope.currentStepNumber()){ + return true; + } + //If canexit is a boolean value instead of a function, return the value + if(typeof step.canexit === 'boolean'){ + return step.canexit; + } + //If canenter is a string instead of a function, evaluate the function + if(typeof step.canexit === 'string'){ + var splitFunction = step.canexit.split('('); + canExit = eval('$scope.$parent.' + splitFunction[0] + '($scope.context' + splitFunction[1]) + } else { + canExit = step.canexit($scope.context); + } + //Check to see if the canexit function is a promise which needs to be returned + if(angular.isFunction(canExit.then)){ + defer = $q.defer(); + canExit.then(function(response){ + defer.resolve(response); + }); + return defer.promise; + } else { + return canExit === true; + } + } + + $scope.currentStepNumber = function() { + //retreive current step number + return stepIdx($scope.selectedStep) + 1; + }; + + $scope.getEnabledSteps = function() { + return $scope.steps.filter(function(step){ + return step && step.disabled !== 'true'; + }); + }; + + //unSelect All Steps function unselectAll() { - _.each($scope.steps, function (step) { + //traverse steps array and set each "selected" property to false + angular.forEach($scope.getEnabledSteps(), function (step) { step.selected = false; }); + //set selectedStep variable to null $scope.selectedStep = null; } - - this.next = function(draft) { - var index = _.indexOf($scope.steps , $scope.selectedStep); - if (!draft) { + + //ALL METHODS ATTACHED TO this ARE ACCESSIBLE VIA WizardHandler.wizard().methodName() + + this.currentStepTitle = function(){ + return $scope.selectedStep.wzTitle; + }; + + this.currentStepDescription = function(){ + return $scope.selectedStep.description; + }; + + this.currentStep = function(){ + return $scope.selectedStep; + }; + + this.totalStepCount = function() { + return $scope.getEnabledSteps().length; + }; + + //Access to enabled steps from outside + this.getEnabledSteps = function(){ + return $scope.getEnabledSteps(); + }; + + //Access to current step number from outside + this.currentStepNumber = function(){ + return $scope.currentStepNumber(); + }; + //method used for next button within step + this.next = function(callback) { + var enabledSteps = $scope.getEnabledSteps(); + //setting variable equal to step you were on when next() was invoked + var index = stepIdx($scope.selectedStep); + //checking to see if callback is a function + if(angular.isFunction(callback)){ + if(callback()){ + if (index === enabledSteps.length - 1) { + this.finish(); + } else { + //invoking goTo() with step number next in line + $scope.goTo(enabledSteps[index + 1]); + } + } else { + return; + } + } + if (!callback) { + //completed property set on scope which is used to add class/remove class from progress bar $scope.selectedStep.completed = true; } - if (index === $scope.steps.length - 1) { + //checking to see if this is the last step. If it is next behaves the same as finish() + if (index === enabledSteps.length - 1) { this.finish(); } else { - $scope.goTo($scope.steps[index + 1]); + //invoking goTo() with step number next in line + $scope.goTo(enabledSteps[index + 1]); } - } - + + }; + + //used to traverse to any step, step number placed as argument this.goTo = function(step) { - var stepTo; - if (_.isNumber(step)) { - stepTo = $scope.steps[step]; - } else { - stepTo = _.find($scope.steps, {title: step}); - } - $scope.goTo(stepTo); - } - + //wrapped inside $timeout so newly enabled steps are included. + $timeout(function() { + var enabledSteps = $scope.getEnabledSteps(); + var stepTo; + //checking that step is a Number + if (angular.isNumber(step)) { + stepTo = enabledSteps[step]; + } else { + //finding the step associated with the title entered as goTo argument + stepTo = stepByTitle(step); + } + //going to step + $scope.goTo(stepTo); + }); + }; + + //calls finish() which calls onFinish() which is declared on an attribute and linked to controller via wizard directive. this.finish = function() { - $scope.onFinish && $scope.onFinish(); - } - - this.cancel = this.previous = function() { - var index = _.indexOf($scope.steps , $scope.selectedStep); + if ($scope.onFinish) { + $scope.onFinish(); + } + }; + + this.previous = function() { + //getting index of current step + var index = stepIdx($scope.selectedStep); + //ensuring you aren't trying to go back from the first step if (index === 0) { throw new Error("Can't go back. It's already in step 0"); } else { - $scope.goTo($scope.steps[index - 1]); + //go back one step from current step + $scope.goTo($scope.getEnabledSteps()[index - 1]); } - } - - + }; + + //cancel is alias for previous. + this.cancel = function() { + if ($scope.onCancel) { + //onCancel is linked to controller via wizard directive: + $scope.onCancel(); + } else { + //getting index of current step + var index = stepIdx($scope.selectedStep); + //ensuring you aren't trying to go back from the first step + if (index === 0) { + throw new Error("Can't go back. It's already in step 0"); + } else { + //go back one step from current step + $scope.goTo($scope.getEnabledSteps()[0]); + } + } + }; + + //reset + this.reset = function(){ + //traverse steps array and set each "completed" property to false + angular.forEach($scope.getEnabledSteps(), function (step) { + step.completed = false; + }); + //go to first step + this.goTo(0); + }; + + //change edit mode + this.setEditMode = function(mode) { + $scope.editMode = mode; + handleEditModeChange(); + }; }] - } -}); \ No newline at end of file + }; +}); diff --git a/src/wizardButtons.js b/src/wizardButtons.js index 7e46170..0afb46f 100644 --- a/src/wizardButtons.js +++ b/src/wizardButtons.js @@ -1,27 +1,26 @@ function wizardButtonDirective(action) { angular.module('mgo-angular-wizard') - .directive(action, ['$parse', function($parse) { + .directive(action, function() { return { restrict: 'A', replace: false, require: '^wizard', link: function($scope, $element, $attrs, wizard) { - + $element.on("click", function(e) { e.preventDefault(); $scope.$apply(function() { - var fn = $parse($attrs[action]); - fn && fn(); + $scope.$eval($attrs[action]); wizard[action.replace("wz", "").toLowerCase()](); }); }); } - } - }]); + }; + }); } wizardButtonDirective('wzNext'); wizardButtonDirective('wzPrevious'); wizardButtonDirective('wzFinish'); wizardButtonDirective('wzCancel'); - +wizardButtonDirective('wzReset'); diff --git a/src/wizardHandler.js b/src/wizardHandler.js index 771b757..8957c94 100644 --- a/src/wizardHandler.js +++ b/src/wizardHandler.js @@ -7,11 +7,11 @@ angular.module('mgo-angular-wizard').factory('WizardHandler', function() { service.addWizard = function(name, wizard) { wizards[name] = wizard; - } + }; - service.deleteWizard = function(name) { + service.removeWizard = function(name) { delete wizards[name]; - } + }; service.wizard = function(name) { var nameToUse = name; @@ -20,7 +20,7 @@ angular.module('mgo-angular-wizard').factory('WizardHandler', function() { } return wizards[nameToUse]; - } + }; return service; -}); \ No newline at end of file +}); diff --git a/test/angularWizardSpec.js b/test/angularWizardSpec.js index f988840..708230a 100644 --- a/test/angularWizardSpec.js +++ b/test/angularWizardSpec.js @@ -1,7 +1,391 @@ describe( 'AngularWizard', function() { - beforeEach( module( 'mgo-angular-wizard' ) ); + var $compile, $q, $rootScope, $timeout, WizardHandler; - it( 'should have a dummy test', inject( function() { - expect( true ).toBeTruthy(); - })); -}); \ No newline at end of file + beforeEach(module('mgo-angular-wizard')); + + + beforeEach(inject(function(_$compile_, _$q_, _$rootScope_, _$timeout_, _WizardHandler_) { + $compile = _$compile_; + $q = _$q_; + $rootScope = _$rootScope_; + $timeout = _$timeout_; + WizardHandler = _WizardHandler_; + })); + + /** + * Create the generic view with wizard to test + * @param {Scope} scope A scope to bind to + * @return {[DOM element]} A DOM element compiled + */ + function createGenericView(scope) { + scope.referenceCurrentStep = null; + var element = angular.element('' + + ' ' + + '

This is the first step

' + + '

Here you can use whatever you want. You can use other directives, binding, etc.

' + + ' ' + + '
' + + ' ' + + '

Dynamic {{dynamicStepDisabled}}

' + + '

You have continued here!

' + + ' ' + + '
' + + ' ' + + '

Continuing

' + + '

You have continued here!

' + + ' ' + + '
' + + ' ' + + '

Even more steps!!

' + + ' ' + + '
' + + '
'); + var elementCompiled = $compile(element)(scope); + $rootScope.$digest(); + return elementCompiled; + } + + it("should correctly create the wizard", function() { + var scope = $rootScope.$new(); + var view = createGenericView(scope); + expect(WizardHandler).toBeTruthy(); + expect(view.find('section').length).toEqual(4); + // expect the correct step to be desirable one + expect(scope.referenceCurrentStep).toEqual('Starting'); + }); + it( "should go to the next step", function() { + var scope = $rootScope.$new(); + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Dynamic'); + }); + it( "should render only those steps which are enabled", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + }); + it( "should enable or disable dynamic steps based on conditions", function() { + var scope = $rootScope.$new(); + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + scope.dynamicStepDisabled = 'Y'; + $rootScope.$digest(); + WizardHandler.wizard().goTo(2); + $rootScope.$digest(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should return to a previous step", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().previous(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Starting'); + }); + it( "should go to a step specified by name", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().goTo('More steps'); + $rootScope.$digest(); + $timeout.flush() + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should go to a step specified by index", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().goTo(2); + $rootScope.$digest(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should go to next step becasue callback is truthy", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(function(){ + return true + }); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + }); + it( "should NOT go to next step because callback is falsey", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(function(){ + return false + }); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Starting'); + }); + it( "should go to next step because CANEXIT is UNDEFINED", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + }); + it( "should go to next step because CANEXIT is TRUE", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.stepValidation = function(){ + return true; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should NOT go to next step because CANEXIT is FALSE", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.stepValidation = function(){ + return false; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + }); + it( "should go to next step because CANENTER is TRUE", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.enterValidation = function(){ + return true; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should NOT go to next step because CANENTER is FALSE", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.enterValidation = function(){ + return false; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + }); + it( "should NOT return to a previous step. Although CANEXIT is false and we are heading to a previous state, the can enter validation is false", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.stepValidation = function(){ + return false; + }; + scope.enterValidation = function(){ + return false; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().previous(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + }); + it( "should return to a previous step even though CANEXIT is false", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.stepValidation = function(){ + return false; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().previous(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Starting'); + }); + it( "should go to the next step because the promise that CANENTER returns resolves to true", function(done) { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.enterValidation = function(){ + var deferred = $q.defer(); + $timeout(function () { + deferred.resolve(true); + done(); + }); + return deferred.promise; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should go to the next step because the promise that the evaluated string of CANENTER resolves to true", function(done) { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.enterValidation = 'stringEnterValidation()' + scope.stringEnterValidation = function(){ + var deferred = $q.defer(); + $timeout(function () { + deferred.resolve(true); + done(); + }); + return deferred.promise; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should go to the next step because CANEXIT is set to true", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.exitValidation = true; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should go to the next step because the promise that the evaluated string of CANEXIT resolves to true", function(done) { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + scope.stepValidation = 'stringStepValidation()' + scope.stringStepValidation = function(){ + var deferred = $q.defer(); + $timeout(function () { + deferred.resolve(true); + done(); + }); + return deferred.promise; + }; + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().next(); + $rootScope.$digest(); + expect(scope.referenceCurrentStep).toEqual('Continuing'); + WizardHandler.wizard().next(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + }); + it( "should finish", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var flag = false; + scope.finishedWizard = function() { flag = true; }; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().finish(); + expect(flag).toBeTruthy(); + $rootScope.$digest(); + }); + it( "should go to first step when reset is called", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(scope.referenceCurrentStep).toEqual('Starting'); + WizardHandler.wizard().goTo(2); + $rootScope.$digest(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('More steps'); + WizardHandler.wizard().reset(); + $rootScope.$digest(); + $timeout.flush(); + expect(scope.referenceCurrentStep).toEqual('Starting'); + }); + it( "step description should be accessible", function() { + var scope = $rootScope.$new(); + scope.dynamicStepDisabled = 'Y'; + var view = createGenericView(scope); + expect(view.isolateScope().steps[0].description).toEqual('Step description'); + }); + it("should create and delete dynamic steps", function () { + var scope = $rootScope.$new(); + scope.steps = ["1", "2", "3", "4"]; + var element = angular.element('{{step}}'); + var view = $compile(element)(scope); + $rootScope.$digest(); + expect(WizardHandler.wizard().getEnabledSteps().length).toBe(4); + view.isolateScope().steps = ["5", "6", "7"]; + view.isolateScope().$digest(); + expect(WizardHandler.wizard().getEnabledSteps().length).toBe(3); + }); + it("should add steps based on order", function () { + var scope = $rootScope.$new(); + scope.steps = [{title: "One", order: 1}, {title: "Zero", order: 0}]; + var element = angular.element('{{step.title}}'); + var view = $compile(element)(scope); + $rootScope.$digest(); + expect(WizardHandler.wizard().totalStepCount()).toBe(2); + expect(view.isolateScope().steps[0].wzTitle).toEqual(scope.steps[1].title); + }); + it("should append step to end if step at order exists", function () { + var scope = $rootScope.$new(); + scope.steps = [{title: "One", order: 1}, {title: "Zero", order: 0}]; + var element = angular.element('{{step.title}}'); + var view = $compile(element)(scope); + $rootScope.$digest(); + var newStep = $rootScope.$new(); + newStep.wzTitle = 'New Step'; + newStep.wzOrder = 0; + WizardHandler.wizard().addStep(newStep); + $rootScope.$digest(); + expect(WizardHandler.wizard().totalStepCount()).toBe(3); + expect(view.isolateScope().steps[2].wzTitle).toEqual(newStep.wzTitle); + }); + it("should always append to end if no order defined", function () { + var scope = $rootScope.$new(); + scope.steps = [{title: "One"}, {title: "Zero"}, {title: "Two"}]; + var element = angular.element('{{step.title}}'); + var view = $compile(element)(scope); + $rootScope.$digest(); + expect(WizardHandler.wizard().totalStepCount()).toBe(3); + scope.steps.forEach(function (step, index) { + expect(view.isolateScope().steps[index].wzTitle).toEqual(step.title); + }); + }); +});