├── .gitignore ├── .travis.yml ├── bower.json ├── package.json ├── src ├── animations.less ├── styles.less ├── ngWizardTemplate.html └── ngWizardDirective.js ├── gruntfile.js ├── karma.conf.js ├── tmp └── templates.js ├── README.md ├── dist ├── ngWizard.min.css ├── ngWizard.css ├── ngWizard.min.js └── ngWizard.js └── test └── wizard.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | tmp/ 4 | bower_components/ 5 | node_modules/ 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli 6 | - npm install -g bower 7 | - bower install -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-wizard", 3 | "version": "1.1.1", 4 | "authors": [ 5 | "David Martin " 6 | ], 7 | "description": "A wizard directive for angular", 8 | "keywords": [ 9 | "angular" 10 | ], 11 | "license": "MIT", 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test", 17 | "tests", 18 | "src", 19 | "tmp" 20 | ], 21 | "dependencies": { 22 | "bootstrap-css": "~3.3.6", 23 | "angular": "1.4.9", 24 | "angular-animate": "~1.4.9", 25 | "font-awesome": "~4.5.0", 26 | "angular-tooltips": "~1.0.6" 27 | }, 28 | "resolutions": { 29 | "angular": "1.4.9" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngWizard", 3 | "version": "1.0.0", 4 | "description": "A wizard directive for angular", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "grunt -v" 11 | }, 12 | "author": "DaveWM", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "angular-mocks": "1.4.9", 16 | "grunt": "^0.4.5", 17 | "grunt-contrib-jshint": "^0.12.0", 18 | "grunt-contrib-less": "^1.0.0", 19 | "grunt-contrib-uglify": "^0.11.0", 20 | "grunt-html2js": "^0.3.5", 21 | "grunt-karma": "^0.12.1", 22 | "jasmine-core": "^2.1.3", 23 | "karma": "^0.13.19", 24 | "karma-jasmine": "^0.3.4", 25 | "karma-phantomjs-launcher": "^0.2.3", 26 | "karma-verbose-reporter": "0.0.3", 27 | "load-grunt-tasks": "^3.4.0", 28 | "phantomjs": "^1.9.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/animations.less: -------------------------------------------------------------------------------- 1 | .animate{ 2 | .transform(@transform){ 3 | transform: @transform; 4 | -ms-transform: @transform; 5 | -moz-transform: @transform; 6 | -webkit-transform: @transform; 7 | } 8 | .filter(@filter){ 9 | -webkit-filter: @filter; 10 | -moz-filter: @filter; 11 | -o-filter: @filter; 12 | -ms-filter: @filter; 13 | } 14 | .animation(@start, @end){ 15 | &.ng-hide-remove-active, &.ng-hide-add-active{ 16 | -webkit-transition: 0.5s all ease; 17 | position:relative; 18 | } 19 | &.ng-hide-remove.ng-hide-remove-active{ 20 | @end(); 21 | } 22 | &.ng-hide-remove{ 23 | @start(); 24 | } 25 | &.ng-hide-add{ 26 | @end(); 27 | } 28 | &.ng-hide-add.ng-hide-add-active{ 29 | @start(); 30 | } 31 | } 32 | .showAnimationOnly(@start, @end){ 33 | .animation(@start, @end); 34 | &.ng-hide-add{ 35 | display: none !important; 36 | } 37 | } 38 | 39 | &.slide{ 40 | .showAnimationOnly({.transform(translateX(20%)); opacity: 0.5;}, {.transform(translateX(0)); opacity: 1;}); 41 | } 42 | &.fade-in{ 43 | .showAnimationOnly({opacity: 0;}, {opacity: 1;}); 44 | } 45 | &.fade-in-out{ 46 | .animation({opacity: 0;}, {opacity: 1;}); 47 | } 48 | &.zoom{ 49 | .showAnimationOnly({ 50 | .transform(translateZ(-250px)); 51 | .filter(opacity(10%)); 52 | },{ 53 | .transform(translateZ(0)); 54 | .filter(opacity(100%)); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/styles.less: -------------------------------------------------------------------------------- 1 | @import "./animations.less"; 2 | @import (inline) "../bower_components/angular-tooltips/dist/angular-tooltips.min.css"; 3 | 4 | 5 | .wizard-container { 6 | background-color: #e7e7e7; 7 | 8 | .wizard-main{ 9 | padding: 20px; 10 | .pager { 11 | i { 12 | -webkit-transition: all 0.3s ease; 13 | &.selected{ 14 | color: #38a7e2; 15 | -webkit-transform: translateY(-3px); 16 | } 17 | &.disabled{ 18 | color:gray; 19 | cursor: not-allowed; 20 | } 21 | } 22 | a { 23 | cursor: pointer; 24 | } 25 | } 26 | .wizard-step-container{ 27 | // this is to prevent sliding animation making the container wider than it should be 28 | overflow-x: hidden; 29 | // needed for 3d transforms to work, don't know why 30 | -webkit-perspective: 800px; 31 | -moz-perspective: 800px; 32 | -ms-perspective: 800px; 33 | perspective: 800px; 34 | } 35 | .wizard-step{ 36 | background-color: white; 37 | padding:15px; 38 | box-shadow: gray 5px 10px 30px; 39 | display: block; 40 | } 41 | } 42 | .wizard-sidebar{ 43 | margin-top:65px; 44 | margin-bottom: 30px; 45 | background-color: white; 46 | .progress{ 47 | margin: 0; 48 | } 49 | li{ 50 | &.active a{ 51 | margin-left: 15px; 52 | } 53 | a{ 54 | -ms-transition: margin 0.5s ease; 55 | -moz-transition: margin 0.5s ease; 56 | -webkit-transition: margin 0.5s ease; 57 | transition: margin 0.5s ease; 58 | } 59 | } 60 | } 61 | .btn.submit{ 62 | margin-bottom: 5px; 63 | i.ng-hide-add{ 64 | // this is to prevent angular waiting for the spinning animation to stop before hiding the loading spinner 65 | display:none; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (grunt){ 3 | require('load-grunt-tasks')(grunt); 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | karma:{ 8 | unit:{ 9 | configFile: 'karma.conf.js', 10 | singleRun: true 11 | } 12 | }, 13 | less:{ 14 | normal:{ 15 | src: ['src/styles.less'], 16 | dest: 'dist/ngWizard.css' 17 | }, 18 | min:{ 19 | src: ['src/styles.less'], 20 | dest: 'dist/ngWizard.min.css', 21 | options:{ 22 | compress: true 23 | } 24 | } 25 | }, 26 | uglify:{ 27 | normal:{ 28 | src: ['src/**/*.js', 'tmp/**/*.js', 'bower_components/angular-tooltips/dist/angular-tooltips.js'], 29 | dest: 'dist/ngWizard.js', 30 | options:{ 31 | mangle: false, 32 | compress: false, 33 | beautify: true 34 | } 35 | }, 36 | min:{ 37 | src: ['src/**/*.js', 'tmp/**/*.js', 'bower_components/angular-tooltips/dist/angular-tooltips.min.js'], 38 | dest: 'dist/ngWizard.min.js', 39 | options:{ 40 | mangle: true, 41 | compress: true, 42 | beautify: false 43 | } 44 | } 45 | }, 46 | html2js:{ 47 | all:{ 48 | src: ['src/**/*.html'], 49 | dest: 'tmp/templates.js', 50 | options: { 51 | module: 'ngWizardTemplates', 52 | base: 'src' 53 | } 54 | } 55 | }, 56 | jshint:{ 57 | all:{ 58 | src: ['src/**/*.js'] 59 | } 60 | } 61 | }); 62 | 63 | grunt.registerTask('default', ['html2js', 'jshint', 'karma', 'less:normal','less:min', 'uglify:normal', 'uglify:min']); 64 | grunt.registerTask('test', ['html2js','karma']); 65 | } 66 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Jan 10 2015 16:23:21 GMT+0000 (GMT Standard Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'bower_components/angular/angular.js', 19 | 'node_modules/angular-mocks/angular-mocks.js', 20 | 'bower_components/angular-animate/angular-animate.js', 21 | 'bower_components/angular-tooltips/dist/angular-tooltips.js', 22 | 'src/**/*.js', 23 | 'test/**/*.spec.js', 24 | 'tmp/**/*.js' 25 | ], 26 | 27 | 28 | // list of files to exclude 29 | exclude: [ 30 | ], 31 | 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | }, 37 | 38 | 39 | // test results reporter to use 40 | // possible values: 'dots', 'progress' 41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 42 | reporters: ['progress', 'verbose'], 43 | 44 | 45 | // web server port 46 | port: 9876, 47 | 48 | 49 | // enable / disable colors in the output (reporters and logs) 50 | colors: true, 51 | 52 | 53 | // level of logging 54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | logLevel: config.LOG_INFO, 56 | 57 | 58 | // enable / disable watching file and executing tests whenever any file changes 59 | autoWatch: true, 60 | 61 | 62 | // start these browsers 63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 64 | browsers: ['PhantomJS'], 65 | 66 | 67 | // Continuous Integration mode 68 | // if true, Karma captures browsers, runs the tests and exits 69 | singleRun: false 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /src/ngWizardTemplate.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 14 |
15 |
16 | 24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /tmp/templates.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWizardTemplates', ['ngWizardTemplate.html']); 2 | 3 | angular.module("ngWizardTemplate.html", []).run(["$templateCache", function($templateCache) { 4 | $templateCache.put("ngWizardTemplate.html", 5 | "
\n" + 6 | "
\n" + 7 | " \n" + 18 | "
\n" + 19 | "
\n" + 20 | " \n" + 28 | "
\n" + 29 | "
\n" + 30 | "
\n" + 31 | "
\n" + 32 | " \n" + 33 | "
\n" + 34 | "
\n" + 35 | "
\n" + 36 | ""); 37 | }]); 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngWizard [![Build Status](https://travis-ci.org/DaveWM/ngWizard.svg?branch=master)](https://travis-ci.org/DaveWM/ngWizard) [![bitHound Score](https://www.bithound.io/DaveWM/ngWizard/badges/score.svg)](https://www.bithound.io/DaveWM/ngWizard) 2 | ngWizard is an angular directive for creating an animated and reponsive wizard style form, you can see a [demo here](http://embed.plnkr.co/qj3loQYo4QBjcpEKCcEE/preview) 3 | >**Important Note** This directive is not compatible with angular 1.2.x, because of [a bug with nested transclusion](https://github.com/angular/angular.js/issues/6435). 4 | >You need to use angular 1.3.0 or later. 5 | 6 | ## Installing ngWizard 7 | - Either run `bower install ng-wizard --save`, or download the repo and add `dist/ngWizard.js` and `dist/ngWizard.css` to your project 8 | - Reference `ngWizard.js` and `ngWizard.css` in your HTML, e.g. 9 | ```html 10 | 11 | 12 | ``` 13 | - Add a dependency on the `ngWizard` module to your ng-app module: 14 | ```js 15 | angular.module('app',['ngWizard']); 16 | ``` 17 | 18 | ## Using the Wizard 19 | ngWizard adds 2 new HTML tags: `` and ``. You use them like this: 20 | ``` html 21 | 22 | 23 |

step 1

24 | 25 |
26 | 27 |

step 2

28 |
29 |
30 | ``` 31 | The `` element wraps one or more `` elements. 32 | The wizard will display each step in the order they appear in the HTML. 33 | Each wizard step acts like a form, and a submit button will show only when all steps have been filled out and are valid. 34 | In the above example, the input in step 1 is required, so it must be filled in before you can submit the wizard. 35 | 36 | By default, the user can select any step they want straight away, i.e. you can move to a step without completing all previous steps. 37 | If you want the user to complete a step before moving to the next, you can use the `required-step-number` attribute on the ``. 38 | This will disable the step until the required step is completed. 39 | 40 | To localise the text in the previous, next and submit buttons, simply update the `wizardConfigProvider` like so: 41 | 42 | ```js 43 | angular.module('app', ['ngWizard']) 44 | .config(function (wizardConfigProvider) { 45 | wizardConfigProvider.nextString = 'Next'; 46 | wizardConfigProvider.prevString = 'Previous'; 47 | wizardConfigProvider.submitString = 'Submit'; 48 | }) 49 | ``` 50 | 51 | ### Wizard Attributes 52 | 53 | | Attribute | Type | Description | 54 | |-----------|-------|------------| 55 | |`current-step-number`| integer | A reference to the current step number (0 indexed). This must be set. | 56 | |`submit` | function | The function that is called when the submit button is clicked. | 57 | 58 | ### Wizard Step Attributes 59 | 60 | | Attribute | Type | Description | 61 | |-----------|-------|------------| 62 | | `title` | string | The title of the step. | 63 | | `required-step-number` | integer | The step that is required before this step can be navigated to (0 indexed). | 64 | | `entered` | function | Function to call when this step is entered. | 65 | | `animation` | string | Determines the type animation for entering this step. By default, you can choose `slide`, `zoom` or `fade-in`. If you want to add your own animations, you can add them in css [as in the angular documentation](https://docs.angularjs.org/api/ng/directive/ngShow#animations).| 66 | 67 | ## Using a custom template 68 | 69 | To change how the wizard is displayed, you need to override the `ngWizardTemplate.html` template in `$templateCache`. The default template can be found at `src/ngWizardTemplate.html`. For more information, please see the [angularjs $templateCache documentation.](https://docs.angularjs.org/api/ng/service/$templateCache) 70 | 71 | ## Dependencies 72 | ngWizard has dependencies on: 73 | - [Font Awesome](http://fortawesome.github.io/Font-Awesome/) 74 | - [Bootstrap](http://getbootstrap.com/) 75 | - The angular `ngAnimate` module 76 | 77 | ## Contributing 78 | Any suggestions for new features or bug fixes are welcome, and I'll try to review any PRs as quickly as possible. 79 | -------------------------------------------------------------------------------- /dist/ngWizard.min.css: -------------------------------------------------------------------------------- 1 | .animate.slide.ng-hide-remove-active,.animate.slide.ng-hide-add-active{-webkit-transition:.5s all ease;position:relative}.animate.slide.ng-hide-remove.ng-hide-remove-active{transform:translateX(0);-ms-transform:translateX(0);-moz-transform:translateX(0);-webkit-transform:translateX(0);opacity:1}.animate.slide.ng-hide-remove{transform:translateX(20%);-ms-transform:translateX(20%);-moz-transform:translateX(20%);-webkit-transform:translateX(20%);opacity:.5}.animate.slide.ng-hide-add{transform:translateX(0);-ms-transform:translateX(0);-moz-transform:translateX(0);-webkit-transform:translateX(0);opacity:1}.animate.slide.ng-hide-add.ng-hide-add-active{transform:translateX(20%);-ms-transform:translateX(20%);-moz-transform:translateX(20%);-webkit-transform:translateX(20%);opacity:.5}.animate.slide.ng-hide-add{display:none !important}.animate.fade-in.ng-hide-remove-active,.animate.fade-in.ng-hide-add-active{-webkit-transition:.5s all ease;position:relative}.animate.fade-in.ng-hide-remove.ng-hide-remove-active{opacity:1}.animate.fade-in.ng-hide-remove{opacity:0}.animate.fade-in.ng-hide-add{opacity:1}.animate.fade-in.ng-hide-add.ng-hide-add-active{opacity:0}.animate.fade-in.ng-hide-add{display:none !important}.animate.fade-in-out.ng-hide-remove-active,.animate.fade-in-out.ng-hide-add-active{-webkit-transition:.5s all ease;position:relative}.animate.fade-in-out.ng-hide-remove.ng-hide-remove-active{opacity:1}.animate.fade-in-out.ng-hide-remove{opacity:0}.animate.fade-in-out.ng-hide-add{opacity:1}.animate.fade-in-out.ng-hide-add.ng-hide-add-active{opacity:0}.animate.zoom.ng-hide-remove-active,.animate.zoom.ng-hide-add-active{-webkit-transition:.5s all ease;position:relative}.animate.zoom.ng-hide-remove.ng-hide-remove-active{transform:translateZ(0);-ms-transform:translateZ(0);-moz-transform:translateZ(0);-webkit-transform:translateZ(0);-webkit-filter:opacity(100%);-moz-filter:opacity(100%);-o-filter:opacity(100%);-ms-filter:opacity(100%)}.animate.zoom.ng-hide-remove{transform:translateZ(-250px);-ms-transform:translateZ(-250px);-moz-transform:translateZ(-250px);-webkit-transform:translateZ(-250px);-webkit-filter:opacity(10%);-moz-filter:opacity(10%);-o-filter:opacity(10%);-ms-filter:opacity(10%)}.animate.zoom.ng-hide-add{transform:translateZ(0);-ms-transform:translateZ(0);-moz-transform:translateZ(0);-webkit-transform:translateZ(0);-webkit-filter:opacity(100%);-moz-filter:opacity(100%);-o-filter:opacity(100%);-ms-filter:opacity(100%)}.animate.zoom.ng-hide-add.ng-hide-add-active{transform:translateZ(-250px);-ms-transform:translateZ(-250px);-moz-transform:translateZ(-250px);-webkit-transform:translateZ(-250px);-webkit-filter:opacity(10%);-moz-filter:opacity(10%);-o-filter:opacity(10%);-ms-filter:opacity(10%)}.animate.zoom.ng-hide-add{display:none !important}/* 2 | * angular-tooltips 3 | * 1.0.10 4 | * 5 | * Angular.js tooltips module. 6 | * http://720kb.github.io/angular-tooltips 7 | * 8 | * MIT license 9 | * Sat Apr 30 2016 10 | */ 11 | tooltip._bottom tip tip-arrow,tooltip._top tip tip-arrow{border-left:6px solid transparent;border-right:6px solid transparent;left:50%;margin-left:-6px}._exradicated-tooltip{position:absolute;display:block;opacity:1;z-index:999}tooltip{display:inline-block;position:relative}@-webkit-keyframes animate-tooltip{0%{opacity:0}50%{opacity:.5}60%{opacity:.8}70%{opacity:.9}90%{opacity:1}}@-moz-keyframes animate-tooltip{0%{opacity:0}50%{opacity:.5}60%{opacity:.8}70%{opacity:.9}90%{opacity:1}}@-ms-keyframes animate-tooltip{tooltip 0%{opacity:0}tooltip 50%{opacity:.5}tooltip 60%{opacity:.8}tooltip 70%{opacity:.9}tooltip 90%{opacity:1}}@keyframes animate-tooltip{0%{opacity:0}50%{opacity:.5}60%{opacity:.8}70%{opacity:.9}90%{opacity:1}}tooltip._multiline{display:block}tooltip._slow._ready tip{animation:animate-tooltip .65s}tooltip._fast._ready tip{animation:animate-tooltip .15s}tooltip._steady._ready tip{animation:animate-tooltip .35s}tooltip tip{border-radius:3px;background:rgba(0,0,0,.85);color:#fff;display:none;line-height:normal;max-width:500px;min-width:100px;opacity:0;padding:8px 16px;position:absolute;text-align:center;width:auto;will-change:top,left,bottom,right}tooltip tip._hidden{display:block;visibility:hidden}tooltip.active:not(._force-hidden) tip{display:block;opacity:1;z-index:999}tooltip tip-tip{font-size:.95em}tooltip tip-tip._large{font-size:1.1em}tooltip tip-tip._small{font-size:.8em}tooltip._top tip{left:50%;top:-9px;-webkit-transform:translateX(-50%) translateY(-100%);transform:translateX(-50%) translateY(-100%)}tooltip._top tip tip-arrow{border-top:6px solid rgba(0,0,0,.85);content:'';height:0;position:absolute;top:100%;width:0}tooltip._bottom tip{right:50%;top:100%;-webkit-transform:translateX(50%) translateY(9px);transform:translateX(50%) translateY(9px)}tooltip._bottom tip tip-arrow{border-bottom:6px solid rgba(0,0,0,.85);bottom:100%;content:'';height:0;position:absolute;width:0}tooltip._left tip tip-arrow,tooltip._right tip tip-arrow{border-bottom:6px solid transparent;border-top:6px solid transparent;content:'';height:0;margin-top:-6px;position:absolute;top:50%;width:0}tooltip._right tip{left:100%;top:50%;-webkit-transform:translateX(9px) translateY(-50%);transform:translateX(9px) translateY(-50%)}tooltip._right tip tip-arrow{border-right:6px solid rgba(0,0,0,.85);right:100%}tooltip._left tip{left:-9px;top:50%;-webkit-transform:translateX(-100%) translateY(-50%);transform:translateX(-100%) translateY(-50%)}tooltip._left tip tip-arrow{border-left:6px solid rgba(0,0,0,.85);left:100%}tip-tip #close-button{cursor:pointer;float:right;left:8%;margin-top:-7%;padding:3px;position:relative} 12 | /*# sourceMappingURL=angular-tooltips.css.map */ 13 | .wizard-container{background-color:#e7e7e7}.wizard-container .wizard-main{padding:20px}.wizard-container .wizard-main .pager i{-webkit-transition:all .3s ease}.wizard-container .wizard-main .pager i.selected{color:#38a7e2;-webkit-transform:translateY(-3px)}.wizard-container .wizard-main .pager i.disabled{color:gray;cursor:not-allowed}.wizard-container .wizard-main .pager a{cursor:pointer}.wizard-container .wizard-main .wizard-step-container{overflow-x:hidden;-webkit-perspective:800px;-moz-perspective:800px;-ms-perspective:800px;perspective:800px}.wizard-container .wizard-main .wizard-step{background-color:white;padding:15px;box-shadow:gray 5px 10px 30px;display:block}.wizard-container .wizard-sidebar{margin-top:65px;margin-bottom:30px;background-color:white}.wizard-container .wizard-sidebar .progress{margin:0}.wizard-container .wizard-sidebar li.active a{margin-left:15px}.wizard-container .wizard-sidebar li a{-ms-transition:margin .5s ease;-moz-transition:margin .5s ease;-webkit-transition:margin .5s ease;transition:margin .5s ease}.wizard-container .btn.submit{margin-bottom:5px}.wizard-container .btn.submit i.ng-hide-add{display:none} -------------------------------------------------------------------------------- /dist/ngWizard.css: -------------------------------------------------------------------------------- 1 | .animate.slide.ng-hide-remove-active, 2 | .animate.slide.ng-hide-add-active { 3 | -webkit-transition: 0.5s all ease; 4 | position: relative; 5 | } 6 | .animate.slide.ng-hide-remove.ng-hide-remove-active { 7 | transform: translateX(0); 8 | -ms-transform: translateX(0); 9 | -moz-transform: translateX(0); 10 | -webkit-transform: translateX(0); 11 | opacity: 1; 12 | } 13 | .animate.slide.ng-hide-remove { 14 | transform: translateX(20%); 15 | -ms-transform: translateX(20%); 16 | -moz-transform: translateX(20%); 17 | -webkit-transform: translateX(20%); 18 | opacity: 0.5; 19 | } 20 | .animate.slide.ng-hide-add { 21 | transform: translateX(0); 22 | -ms-transform: translateX(0); 23 | -moz-transform: translateX(0); 24 | -webkit-transform: translateX(0); 25 | opacity: 1; 26 | } 27 | .animate.slide.ng-hide-add.ng-hide-add-active { 28 | transform: translateX(20%); 29 | -ms-transform: translateX(20%); 30 | -moz-transform: translateX(20%); 31 | -webkit-transform: translateX(20%); 32 | opacity: 0.5; 33 | } 34 | .animate.slide.ng-hide-add { 35 | display: none !important; 36 | } 37 | .animate.fade-in.ng-hide-remove-active, 38 | .animate.fade-in.ng-hide-add-active { 39 | -webkit-transition: 0.5s all ease; 40 | position: relative; 41 | } 42 | .animate.fade-in.ng-hide-remove.ng-hide-remove-active { 43 | opacity: 1; 44 | } 45 | .animate.fade-in.ng-hide-remove { 46 | opacity: 0; 47 | } 48 | .animate.fade-in.ng-hide-add { 49 | opacity: 1; 50 | } 51 | .animate.fade-in.ng-hide-add.ng-hide-add-active { 52 | opacity: 0; 53 | } 54 | .animate.fade-in.ng-hide-add { 55 | display: none !important; 56 | } 57 | .animate.fade-in-out.ng-hide-remove-active, 58 | .animate.fade-in-out.ng-hide-add-active { 59 | -webkit-transition: 0.5s all ease; 60 | position: relative; 61 | } 62 | .animate.fade-in-out.ng-hide-remove.ng-hide-remove-active { 63 | opacity: 1; 64 | } 65 | .animate.fade-in-out.ng-hide-remove { 66 | opacity: 0; 67 | } 68 | .animate.fade-in-out.ng-hide-add { 69 | opacity: 1; 70 | } 71 | .animate.fade-in-out.ng-hide-add.ng-hide-add-active { 72 | opacity: 0; 73 | } 74 | .animate.zoom.ng-hide-remove-active, 75 | .animate.zoom.ng-hide-add-active { 76 | -webkit-transition: 0.5s all ease; 77 | position: relative; 78 | } 79 | .animate.zoom.ng-hide-remove.ng-hide-remove-active { 80 | transform: translateZ(0); 81 | -ms-transform: translateZ(0); 82 | -moz-transform: translateZ(0); 83 | -webkit-transform: translateZ(0); 84 | -webkit-filter: opacity(100%); 85 | -moz-filter: opacity(100%); 86 | -o-filter: opacity(100%); 87 | -ms-filter: opacity(100%); 88 | } 89 | .animate.zoom.ng-hide-remove { 90 | transform: translateZ(-250px); 91 | -ms-transform: translateZ(-250px); 92 | -moz-transform: translateZ(-250px); 93 | -webkit-transform: translateZ(-250px); 94 | -webkit-filter: opacity(10%); 95 | -moz-filter: opacity(10%); 96 | -o-filter: opacity(10%); 97 | -ms-filter: opacity(10%); 98 | } 99 | .animate.zoom.ng-hide-add { 100 | transform: translateZ(0); 101 | -ms-transform: translateZ(0); 102 | -moz-transform: translateZ(0); 103 | -webkit-transform: translateZ(0); 104 | -webkit-filter: opacity(100%); 105 | -moz-filter: opacity(100%); 106 | -o-filter: opacity(100%); 107 | -ms-filter: opacity(100%); 108 | } 109 | .animate.zoom.ng-hide-add.ng-hide-add-active { 110 | transform: translateZ(-250px); 111 | -ms-transform: translateZ(-250px); 112 | -moz-transform: translateZ(-250px); 113 | -webkit-transform: translateZ(-250px); 114 | -webkit-filter: opacity(10%); 115 | -moz-filter: opacity(10%); 116 | -o-filter: opacity(10%); 117 | -ms-filter: opacity(10%); 118 | } 119 | .animate.zoom.ng-hide-add { 120 | display: none !important; 121 | } 122 | /* 123 | * angular-tooltips 124 | * 1.0.10 125 | * 126 | * Angular.js tooltips module. 127 | * http://720kb.github.io/angular-tooltips 128 | * 129 | * MIT license 130 | * Sat Apr 30 2016 131 | */ 132 | tooltip._bottom tip tip-arrow,tooltip._top tip tip-arrow{border-left:6px solid transparent;border-right:6px solid transparent;left:50%;margin-left:-6px}._exradicated-tooltip{position:absolute;display:block;opacity:1;z-index:999}tooltip{display:inline-block;position:relative}@-webkit-keyframes animate-tooltip{0%{opacity:0}50%{opacity:.5}60%{opacity:.8}70%{opacity:.9}90%{opacity:1}}@-moz-keyframes animate-tooltip{0%{opacity:0}50%{opacity:.5}60%{opacity:.8}70%{opacity:.9}90%{opacity:1}}@-ms-keyframes animate-tooltip{tooltip 0%{opacity:0}tooltip 50%{opacity:.5}tooltip 60%{opacity:.8}tooltip 70%{opacity:.9}tooltip 90%{opacity:1}}@keyframes animate-tooltip{0%{opacity:0}50%{opacity:.5}60%{opacity:.8}70%{opacity:.9}90%{opacity:1}}tooltip._multiline{display:block}tooltip._slow._ready tip{animation:animate-tooltip .65s}tooltip._fast._ready tip{animation:animate-tooltip .15s}tooltip._steady._ready tip{animation:animate-tooltip .35s}tooltip tip{border-radius:3px;background:rgba(0,0,0,.85);color:#fff;display:none;line-height:normal;max-width:500px;min-width:100px;opacity:0;padding:8px 16px;position:absolute;text-align:center;width:auto;will-change:top,left,bottom,right}tooltip tip._hidden{display:block;visibility:hidden}tooltip.active:not(._force-hidden) tip{display:block;opacity:1;z-index:999}tooltip tip-tip{font-size:.95em}tooltip tip-tip._large{font-size:1.1em}tooltip tip-tip._small{font-size:.8em}tooltip._top tip{left:50%;top:-9px;-webkit-transform:translateX(-50%) translateY(-100%);transform:translateX(-50%) translateY(-100%)}tooltip._top tip tip-arrow{border-top:6px solid rgba(0,0,0,.85);content:'';height:0;position:absolute;top:100%;width:0}tooltip._bottom tip{right:50%;top:100%;-webkit-transform:translateX(50%) translateY(9px);transform:translateX(50%) translateY(9px)}tooltip._bottom tip tip-arrow{border-bottom:6px solid rgba(0,0,0,.85);bottom:100%;content:'';height:0;position:absolute;width:0}tooltip._left tip tip-arrow,tooltip._right tip tip-arrow{border-bottom:6px solid transparent;border-top:6px solid transparent;content:'';height:0;margin-top:-6px;position:absolute;top:50%;width:0}tooltip._right tip{left:100%;top:50%;-webkit-transform:translateX(9px) translateY(-50%);transform:translateX(9px) translateY(-50%)}tooltip._right tip tip-arrow{border-right:6px solid rgba(0,0,0,.85);right:100%}tooltip._left tip{left:-9px;top:50%;-webkit-transform:translateX(-100%) translateY(-50%);transform:translateX(-100%) translateY(-50%)}tooltip._left tip tip-arrow{border-left:6px solid rgba(0,0,0,.85);left:100%}tip-tip #close-button{cursor:pointer;float:right;left:8%;margin-top:-7%;padding:3px;position:relative} 133 | /*# sourceMappingURL=angular-tooltips.css.map */ 134 | 135 | .wizard-container { 136 | background-color: #e7e7e7; 137 | } 138 | .wizard-container .wizard-main { 139 | padding: 20px; 140 | } 141 | .wizard-container .wizard-main .pager i { 142 | -webkit-transition: all 0.3s ease; 143 | } 144 | .wizard-container .wizard-main .pager i.selected { 145 | color: #38a7e2; 146 | -webkit-transform: translateY(-3px); 147 | } 148 | .wizard-container .wizard-main .pager i.disabled { 149 | color: gray; 150 | cursor: not-allowed; 151 | } 152 | .wizard-container .wizard-main .pager a { 153 | cursor: pointer; 154 | } 155 | .wizard-container .wizard-main .wizard-step-container { 156 | overflow-x: hidden; 157 | -webkit-perspective: 800px; 158 | -moz-perspective: 800px; 159 | -ms-perspective: 800px; 160 | perspective: 800px; 161 | } 162 | .wizard-container .wizard-main .wizard-step { 163 | background-color: white; 164 | padding: 15px; 165 | box-shadow: gray 5px 10px 30px; 166 | display: block; 167 | } 168 | .wizard-container .wizard-sidebar { 169 | margin-top: 65px; 170 | margin-bottom: 30px; 171 | background-color: white; 172 | } 173 | .wizard-container .wizard-sidebar .progress { 174 | margin: 0; 175 | } 176 | .wizard-container .wizard-sidebar li.active a { 177 | margin-left: 15px; 178 | } 179 | .wizard-container .wizard-sidebar li a { 180 | -ms-transition: margin 0.5s ease; 181 | -moz-transition: margin 0.5s ease; 182 | -webkit-transition: margin 0.5s ease; 183 | transition: margin 0.5s ease; 184 | } 185 | .wizard-container .btn.submit { 186 | margin-bottom: 5px; 187 | } 188 | .wizard-container .btn.submit i.ng-hide-add { 189 | display: none; 190 | } 191 | -------------------------------------------------------------------------------- /src/ngWizardDirective.js: -------------------------------------------------------------------------------- 1 | angular.module("ngWizard", ['720kb.tooltips', 'ngAnimate', 'ngWizardTemplates']) 2 | .directive('wizard', ['$window','$q', function($window, $q) { 3 | "use strict"; 4 | 5 | return { 6 | restrict: 'E', 7 | transclude: true, 8 | scope: { 9 | currentStepNumber: '=', 10 | submit: '&' 11 | }, 12 | templateUrl: "ngWizardTemplate.html", 13 | controller: function($scope, wizardConfigProvider) { 14 | $scope.prevString = wizardConfigProvider.prevString; 15 | $scope.nextString = wizardConfigProvider.nextString; 16 | $scope.submitString = wizardConfigProvider.submitString; 17 | 18 | $scope.currentStepNumber = $scope.currentStepNumber || 0; 19 | 20 | $scope.getCurrentStep = function() { 21 | return $scope.steps[$scope.currentStepNumber]; 22 | }; 23 | // need to register the method on the controller as well, so it can be accessed by the wizard steps 24 | this.getCurrentStep = $scope.getCurrentStep; 25 | 26 | $scope.goToStepByReference = function(step) { 27 | var stepNumber = $scope.steps.indexOf(step); 28 | return $scope.goToStep(stepNumber); 29 | }; 30 | 31 | // returns whether the step number is between 0 and the number of steps - 1 32 | var isValidStepNumber = function(stepNumber) { 33 | return stepNumber < $scope.steps.length && stepNumber >= 0; 34 | }; 35 | 36 | $scope.canGoToStep = function(stepNumber) { 37 | if (!isValidStepNumber(stepNumber)) { 38 | return false; 39 | } 40 | var newStep = $scope.steps[stepNumber]; 41 | return $scope.getStepState(newStep) != $scope.stepStatesEnum.disabled; 42 | }; 43 | $scope.goToStep = function(stepNumber) { 44 | if ($scope.canGoToStep(stepNumber)) { 45 | $scope.currentStepNumber = stepNumber; 46 | return true; 47 | } 48 | return false; 49 | }; 50 | 51 | $scope.getStepState = function(step) { 52 | // step requires a previous step to be complete, and it is not 53 | if (step.requiredStepNumber && isValidStepNumber(step.requiredStepNumber) && 54 | $scope.getStepState($scope.steps[step.requiredStepNumber]) != $scope.stepStatesEnum.complete) { 55 | return $scope.stepStatesEnum.disabled; 56 | } 57 | // if form is valid, step is complete 58 | else if (step.stepForm && step.stepForm.$valid) { 59 | return $scope.stepStatesEnum.complete; 60 | } 61 | else return $scope.stepStatesEnum.ready; 62 | }; 63 | 64 | $scope.stepStatesEnum = { 65 | disabled: 0, 66 | ready: 1, 67 | complete: 2 68 | }; 69 | 70 | $scope.goToNext = function() { 71 | $scope.goToStep($scope.currentStepNumber + 1); 72 | }; 73 | $scope.hasNext = function() { 74 | return $scope.steps.length > $scope.currentStepNumber + 1 && 75 | $scope.getStepState($scope.steps[$scope.currentStepNumber + 1]) != $scope.stepStatesEnum.disabled; 76 | }; 77 | $scope.goToPrevious = function() { 78 | $scope.goToStep($scope.currentStepNumber - 1); 79 | }; 80 | $scope.hasPrevious = function() { 81 | return $scope.currentStepNumber > 0; 82 | }; 83 | 84 | $scope.getProgressPercentage = function() { 85 | var completeSteps = $scope.steps.filter(function(step) { 86 | return $scope.getStepState(step) == $scope.stepStatesEnum.complete; 87 | }); 88 | return (completeSteps.length / $scope.steps.length) * 100; 89 | }; 90 | 91 | $scope.steps = []; 92 | // assume steps are registered in order 93 | this.registerStep = function(stepScope) { 94 | $scope.steps.push(stepScope); 95 | }; 96 | this.unregisterStep = function(stepScope) { 97 | var index = $scope.steps.indexOf(stepScope); 98 | if (index >= 0) { 99 | $scope.steps.splice(index, 1); 100 | } 101 | }; 102 | 103 | $scope.isSubmittable = function() { 104 | return $scope.steps.every(function(step) { 105 | return $scope.getStepState(step) == $scope.stepStatesEnum.complete; 106 | }); 107 | }; 108 | $scope.submitting = false; 109 | $scope.onSubmitClicked = function() { 110 | $scope.submitting = true; 111 | $q.when($scope.submit()).then(function() { 112 | $scope.submitting = false; 113 | }); 114 | }; 115 | 116 | $scope.$watch('currentStepNumber', function (val, oldVal) { 117 | // don't do anything if step hasn't changed 118 | if (val != oldVal) { 119 | // try to go to new step number, if it doesn't work don't allow the change. 120 | // if "oldVal" (the previous step number) is not defined/is invalid, go to step 0 (always valid) 121 | if (!$scope.canGoToStep(val)) { 122 | if (oldVal && $scope.canGoToStep(oldVal)) { 123 | $scope.currentStepNumber = oldVal; 124 | } 125 | else $scope.currentStepNumber = 0; 126 | } 127 | // successfully navigated to step 128 | else { 129 | $scope.getCurrentStep().entered(); 130 | } 131 | } 132 | }); 133 | // watch the number of steps, in case we are on the last step and it is removed 134 | $scope.$watch('steps.length', function() { 135 | if (!$scope.getCurrentStep()) { 136 | $scope.currentStepNumber = 0; 137 | } 138 | }, true); 139 | } 140 | }; 141 | }]) 142 | .directive('wizardStep', function() { 143 | return { 144 | require: '^wizard', 145 | restrict: 'E', 146 | transclude: true, 147 | scope: { 148 | title: '@', 149 | // the required step must be completed for this step to be enabled 150 | requiredStepNumber: '@', 151 | entered: '&', 152 | animation: '@' 153 | }, 154 | template: "", 155 | link: function ($scope, element, attrs, wizardCtrl) { 156 | wizardCtrl.registerStep($scope); 157 | $scope.isActive = function() { 158 | return $scope == wizardCtrl.getCurrentStep(); 159 | }; 160 | 161 | $scope.$on("$destroy", function() { 162 | wizardCtrl.unregisterStep($scope); 163 | }); 164 | } 165 | }; 166 | }) 167 | .provider('wizardConfigProvider', function () { 168 | this.nextString = 'Next'; 169 | this.prevString = 'Previous'; 170 | this.submitString = 'Submit'; 171 | 172 | this.$get = function() { 173 | return this; 174 | }; 175 | }); 176 | -------------------------------------------------------------------------------- /test/wizard.spec.js: -------------------------------------------------------------------------------- 1 | describe('wizard directive', function() { 2 | var element,controllerScope, directiveScope, $compile, $rootScope; 3 | 4 | beforeEach(module("ngWizard")); 5 | 6 | beforeEach(inject(function (_$compile_, _$rootScope_) { 7 | $compile = _$compile_; 8 | $rootScope = _$rootScope_; 9 | controllerScope = $rootScope.$new(); 10 | controllerScope.currentStepNumber = 0; 11 | controllerScope.requiredText = null; 12 | controllerScope.submit = function () { }; 13 | controllerScope.stepEntered = function () { }; 14 | 15 | controllerScope.dynamicStepTitles = []; 16 | var rawElement = angular.element('' + 17 | '

step 1

' + 18 | '

step 2

' + 19 | '

step 3 - no required steps

' + 20 | '' + 21 | 'Title is {{title}} ' + 22 | '
'); 23 | element = $compile(rawElement)(controllerScope); 24 | controllerScope.$apply(); 25 | directiveScope = controllerScope.$$childHead; 26 | })); 27 | 28 | it("should compile the html correctly", function () { 29 | expect(element[0].tagName.toLowerCase()).toEqual('wizard'); 30 | expect(element.find('wizard-step').length).toEqual(4); 31 | expect(controllerScope.currentStepNumber).toBe(0); 32 | expect(directiveScope.steps.length).toEqual(4); 33 | expect(directiveScope.steps[0].title).toEqual('step 1'); 34 | expect(element.find('wizard-step').eq(0).find('p').text()).toBe('step 1'); 35 | }); 36 | 37 | it("should use the default text for the next, previous and submit buttons by default", function (){ 38 | var scope = $rootScope.$new(); 39 | var rawElem = angular.element(''); 40 | var element = $compile(rawElem)(scope); 41 | scope.$apply(); 42 | 43 | expect(element[0].querySelector('.previous a').text).toContain('Previous'); 44 | expect(element[0].querySelector('.next a').text).toContain('Next'); 45 | expect(element[0].querySelector('button.submit').textContent).toContain('Submit'); 46 | }); 47 | 48 | it("should use the next, previous and submit text defined in the wizardConfigProvider", inject(function (wizardConfigProvider) { 49 | wizardConfigProvider.nextString = 'aaa'; 50 | wizardConfigProvider.prevString = 'bbb'; 51 | wizardConfigProvider.submitString = 'ccc'; 52 | 53 | var scope = $rootScope.$new(); 54 | var rawElem = angular.element(''); 55 | var element = $compile(rawElem)(scope); 56 | scope.$apply(); 57 | 58 | expect(element[0].querySelector('.previous a').text).toContain('bbb'); 59 | expect(element[0].querySelector('.next a').text).toContain('aaa'); 60 | expect(element[0].querySelector('button.submit').textContent).toContain('ccc'); 61 | })); 62 | 63 | it("should enable a step when the step it depends on is complete", function() { 64 | // fill in required text input in step 0, should then enable step 1 65 | controllerScope.requiredText = "test"; 66 | controllerScope.$apply(); 67 | expect(directiveScope.getStepState(directiveScope.steps[1])).not.toEqual(directiveScope.stepStatesEnum.disabled); 68 | }); 69 | 70 | it("should enable a step if the required-step-number is invalid", function() { 71 | expect(directiveScope.getStepState(directiveScope.steps[3])).not.toEqual(directiveScope.stepStatesEnum.disabled); 72 | }); 73 | 74 | it("should always enable steps which don't depend on another step", function () { 75 | // step 2 has no dependency, should always be enabled 76 | expect(directiveScope.getStepState(directiveScope.steps[2])).not.toEqual(directiveScope.stepStatesEnum.disabled); 77 | }); 78 | 79 | it("should not be able to navigate to a disabled step", function () { 80 | // step 1 depends on step 0, so should be disabled 81 | controllerScope.currentStepNumber = 1; 82 | controllerScope.$apply(); 83 | // should still be on step 0 84 | expect(controllerScope.currentStepNumber).toEqual(0); 85 | }); 86 | 87 | it("should only show the active step", function () { 88 | // step 0 shown 89 | expect(element.find('wizard-step').find('ng-form').eq(0).hasClass('ng-hide')).toBe(false); 90 | expect(element.find('wizard-step').find('ng-form').eq(1).hasClass('ng-hide')).toBe(true); 91 | expect(element.find('wizard-step').find('ng-form').eq(2).hasClass('ng-hide')).toBe(true); 92 | 93 | controllerScope.requiredText = "test"; 94 | controllerScope.$apply(); 95 | controllerScope.currentStepNumber = 1; 96 | controllerScope.$apply(); 97 | 98 | // step 1 shown 99 | expect(element.find('wizard-step').find('ng-form').eq(0).hasClass('ng-hide')).toBe(true); 100 | expect(element.find('wizard-step').find('ng-form').eq(1).hasClass('ng-hide')).toBe(false); 101 | expect(element.find('wizard-step').find('ng-form').eq(2).hasClass('ng-hide')).toBe(true); 102 | }); 103 | 104 | it("should only show the submit button when all steps are complete", function () { 105 | var submitButton = element.find('button'); 106 | expect(submitButton.hasClass('submit')).toBe(true); 107 | expect(submitButton.hasClass('ng-hide')).toBe(true); 108 | expect(directiveScope.isSubmittable()).toBe(false); 109 | 110 | controllerScope.requiredText = "test"; 111 | controllerScope.$apply(); 112 | expect(submitButton.hasClass('ng-hide')).toBe(false); 113 | expect(directiveScope.isSubmittable()).toBe(true); 114 | }); 115 | 116 | it("should call the submit function on the controller when the submit button is clicked", function() { 117 | spyOn(directiveScope, "onSubmitClicked").and.callThrough(); 118 | spyOn(controllerScope, "submit").and.returnValue(true); 119 | 120 | controllerScope.requiredText = "test"; 121 | controllerScope.$apply(); 122 | element.find('button')[0].click(); 123 | controllerScope.$apply(); 124 | 125 | expect(controllerScope.submit).toHaveBeenCalled(); 126 | expect(directiveScope.onSubmitClicked).toHaveBeenCalled(); 127 | }); 128 | 129 | it("should call the entered() function when the user navigates to a step", function () { 130 | spyOn(controllerScope, "stepEntered"); 131 | controllerScope.requiredText = "test"; 132 | controllerScope.$apply(); 133 | controllerScope.currentStepNumber = 1; 134 | controllerScope.$apply(); 135 | 136 | expect(controllerScope.stepEntered.calls.count()).toEqual(1); 137 | 138 | controllerScope.currentStepNumber = 2; 139 | controllerScope.$apply(); 140 | 141 | expect(controllerScope.stepEntered.calls.count()).toEqual(2); 142 | 143 | directiveScope.goToStep(0); 144 | directiveScope.$apply(); 145 | expect(controllerScope.stepEntered.calls.count()).toEqual(3); 146 | }); 147 | 148 | it("should not allow a wizard-step tag to be outside a wizard tag", function() { 149 | var invalidElement = angular.element(''); 150 | // should throw an exception when we compile the wizard-step element 151 | expect($compile(invalidElement)).toThrow(); 152 | }); 153 | 154 | it("should allow steps to be added dynamically", function () { 155 | var stepTitle = "dynamically added step"; 156 | controllerScope.dynamicStepTitles.push(stepTitle); 157 | $rootScope.$apply(); 158 | 159 | expect(directiveScope.steps.length).toEqual(5); 160 | expect(directiveScope.steps[4].title).toEqual(stepTitle); 161 | 162 | controllerScope.dynamicStepTitles = []; 163 | controllerScope.$apply(); 164 | 165 | expect(directiveScope.steps.length).toEqual(4); 166 | }); 167 | 168 | it("should calculate the progress percentage correctly", function() { 169 | // 1 out of 3 steps complete by default 170 | expect(directiveScope.getProgressPercentage()).toBeCloseTo(2 * 100 / 4, 5); 171 | 172 | controllerScope.requiredText = "test"; 173 | controllerScope.$apply(); 174 | 175 | expect(directiveScope.getProgressPercentage()).toEqual(100); 176 | }); 177 | 178 | 179 | it("should correctly determine whether the next/previous buttons are enabled", function () { 180 | var el = angular.element(' ' + 181 | '' + 182 | '' + 183 | '' + 184 | ''); 185 | var parentScope = $rootScope.$new(); 186 | parentScope.currentStepNumber = 0; 187 | $compile(el)(parentScope); 188 | parentScope.$apply(); 189 | var wizardScope = parentScope.$$childHead; 190 | 191 | // on step 1, steps 2 and 3 should be disabled so next and previous buttons should be disabled 192 | expect(wizardScope.hasNext()).toBe(false); 193 | expect(wizardScope.hasPrevious()).toBe(false); 194 | 195 | // complete step 1 196 | parentScope.step1 = "done"; 197 | parentScope.$apply(); 198 | 199 | // should now be able to go next 200 | expect(wizardScope.hasNext()).toBe(true); 201 | expect(wizardScope.hasPrevious()).toBe(false); 202 | 203 | // move to step 2 204 | parentScope.currentStepNumber = 1; 205 | parentScope.$apply(); 206 | 207 | // should now be able to go back, but not forwards (step 3 is disabled) 208 | expect(wizardScope.hasNext()).toBe(false); 209 | expect(wizardScope.hasPrevious()).toBe(true); 210 | 211 | // complete step 2 212 | parentScope.step2 = "done"; 213 | parentScope.$apply(); 214 | 215 | // should now be able to go next and previous 216 | expect(wizardScope.hasNext()).toBe(true); 217 | expect(wizardScope.hasPrevious()).toBe(true); 218 | 219 | // move to step 3 220 | parentScope.currentStepNumber = 2; 221 | parentScope.$apply(); 222 | 223 | // should now be able to go back 224 | expect(wizardScope.hasNext()).toBe(false); 225 | expect(wizardScope.hasPrevious()).toBe(true); 226 | 227 | // complete step 3 228 | parentScope.step3 = "done"; 229 | parentScope.$apply(); 230 | 231 | // should still only be able to go back 232 | expect(wizardScope.hasNext()).toBe(false); 233 | expect(wizardScope.hasPrevious()).toBe(true); 234 | }); 235 | }) 236 | -------------------------------------------------------------------------------- /dist/ngWizard.min.js: -------------------------------------------------------------------------------- 1 | angular.module("ngWizard",["720kb.tooltips","ngAnimate","ngWizardTemplates"]).directive("wizard",["$window","$q",function(a,b){"use strict";return{restrict:"E",transclude:!0,scope:{currentStepNumber:"=",submit:"&"},templateUrl:"ngWizardTemplate.html",controller:function(a,c){a.prevString=c.prevString,a.nextString=c.nextString,a.submitString=c.submitString,a.currentStepNumber=a.currentStepNumber||0,a.getCurrentStep=function(){return a.steps[a.currentStepNumber]},this.getCurrentStep=a.getCurrentStep,a.goToStepByReference=function(b){var c=a.steps.indexOf(b);return a.goToStep(c)};var d=function(b){return b=0};a.canGoToStep=function(b){if(!d(b))return!1;var c=a.steps[b];return a.getStepState(c)!=a.stepStatesEnum.disabled},a.goToStep=function(b){return!!a.canGoToStep(b)&&(a.currentStepNumber=b,!0)},a.getStepState=function(b){return b.requiredStepNumber&&d(b.requiredStepNumber)&&a.getStepState(a.steps[b.requiredStepNumber])!=a.stepStatesEnum.complete?a.stepStatesEnum.disabled:b.stepForm&&b.stepForm.$valid?a.stepStatesEnum.complete:a.stepStatesEnum.ready},a.stepStatesEnum={disabled:0,ready:1,complete:2},a.goToNext=function(){a.goToStep(a.currentStepNumber+1)},a.hasNext=function(){return a.steps.length>a.currentStepNumber+1&&a.getStepState(a.steps[a.currentStepNumber+1])!=a.stepStatesEnum.disabled},a.goToPrevious=function(){a.goToStep(a.currentStepNumber-1)},a.hasPrevious=function(){return a.currentStepNumber>0},a.getProgressPercentage=function(){var b=a.steps.filter(function(b){return a.getStepState(b)==a.stepStatesEnum.complete});return b.length/a.steps.length*100},a.steps=[],this.registerStep=function(b){a.steps.push(b)},this.unregisterStep=function(b){var c=a.steps.indexOf(b);c>=0&&a.steps.splice(c,1)},a.isSubmittable=function(){return a.steps.every(function(b){return a.getStepState(b)==a.stepStatesEnum.complete})},a.submitting=!1,a.onSubmitClicked=function(){a.submitting=!0,b.when(a.submit()).then(function(){a.submitting=!1})},a.$watch("currentStepNumber",function(b,c){b!=c&&(a.canGoToStep(b)?a.getCurrentStep().entered():c&&a.canGoToStep(c)?a.currentStepNumber=c:a.currentStepNumber=0)}),a.$watch("steps.length",function(){a.getCurrentStep()||(a.currentStepNumber=0)},!0)}}}]).directive("wizardStep",function(){return{require:"^wizard",restrict:"E",transclude:!0,scope:{title:"@",requiredStepNumber:"@",entered:"&",animation:"@"},template:"",link:function(a,b,c,d){d.registerStep(a),a.isActive=function(){return a==d.getCurrentStep()},a.$on("$destroy",function(){d.unregisterStep(a)})}}}).provider("wizardConfigProvider",function(){this.nextString="Next",this.prevString="Previous",this.submitString="Submit",this.$get=function(){return this}}),angular.module("ngWizardTemplates",["ngWizardTemplate.html"]),angular.module("ngWizardTemplate.html",[]).run(["$templateCache",function(a){a.put("ngWizardTemplate.html",'
\n
\n \n
\n
\n \n
\n
\n
\n
\n \n
\n
\n
\n')}]),!function(a,b){"use strict";var c="tooltips",d=function(){var a=[],c=0,d=function(d){d-c>=15?(a.forEach(function(a){a()}),c=d):b.console.log("Skipped!")},e=function(){b.requestAnimationFrame(d)},f=function(b){b&&a.push(b)};return{add:function(c){a.length||b.addEventListener("resize",e),f(c)}}}(),e=function(a){var b={};return a.removeAttr(c),void 0!==a.attr("tooltip-template")&&(b["tooltip-template"]=a.attr("tooltip-template"),a.removeAttr("tooltip-template")),void 0!==a.attr("tooltip-template-url")&&(b["tooltip-template-url"]=a.attr("tooltip-template-url"),a.removeAttr("tooltip-template-url")),void 0!==a.attr("tooltip-controller")&&(b["tooltip-controller"]=a.attr("tooltip-controller"),a.removeAttr("tooltip-controller")),void 0!==a.attr("tooltip-side")&&(b["tooltip-side"]=a.attr("tooltip-side"),a.removeAttr("tooltip-side")),void 0!==a.attr("tooltip-show-trigger")&&(b["tooltip-show-trigger"]=a.attr("tooltip-show-trigger"),a.removeAttr("tooltip-show-trigger")),void 0!==a.attr("tooltip-hide-trigger")&&(b["tooltip-hide-trigger"]=a.attr("tooltip-hide-trigger"),a.removeAttr("tooltip-hide-trigger")),void 0!==a.attr("tooltip-smart")&&(b["tooltip-smart"]=a.attr("tooltip-smart"),a.removeAttr("tooltip-smart")),void 0!==a.attr("tooltip-class")&&(b["tooltip-class"]=a.attr("tooltip-class"),a.removeAttr("tooltip-class")),void 0!==a.attr("tooltip-close-button")&&(b["tooltip-close-button"]=a.attr("tooltip-close-button"),a.removeAttr("tooltip-close-button")),void 0!==a.attr("tooltip-size")&&(b["tooltip-size"]=a.attr("tooltip-size"),a.removeAttr("tooltip-size")),void 0!==a.attr("tooltip-speed")&&(b["tooltip-speed"]=a.attr("tooltip-speed"),a.removeAttr("tooltip-speed")),b},f=function(a){return b.getComputedStyle?b.getComputedStyle(a,""):a.currentStyle?a.currentStyle:void 0},g=function(c){for(var d,e,f=b.document.querySelectorAll("._exradicated-tooltip"),g=0,h=f.length;h>g;g+=1)if(d=f.item(g),d&&(e=a.element(d),e.data("_tooltip-parent")&&e.data("_tooltip-parent")===c))return e},h=function(a){var b=g(a);b&&b.remove()},i=function(a){if(a){var c=a[0].getBoundingClientRect();return(c.top<0||c.top>b.document.body.offsetHeight||c.left<0||c.left>b.document.body.offsetWidth||c.bottom<0||c.bottom>b.document.body.offsetHeight||c.right<0||c.right>b.document.body.offsetWidth)&&(a.css({top:"",left:"",bottom:"",right:""}),!0)}throw new Error("You must provide a position")},j=function(){var a={side:"top",showTrigger:"mouseover",hideTrigger:"mouseleave","class":"",smart:!1,closeButton:!1,size:"",speed:"steady"};return{configure:function(b){var c,d=Object.keys(a),e=0;if(b)for(;e1?r.addClass("_multiline"):r.removeClass("_multiline")},F=function(c){if(z.addClass("_hidden"),p.tooltipSmart)switch(p.tooltipSide){case"top":i(z)&&(r.removeClass("_top"),r.addClass("_left"),i(z)&&(r.removeClass("_left"),r.addClass("_bottom"),i(z)&&(r.removeClass("_bottom"),r.addClass("_right"),i(z)&&(r.removeClass("_right"),r.addClass("_top")))));break;case"left":i(z)&&(r.removeClass("_left"),r.addClass("_bottom"),i(z)&&(r.removeClass("_bottom"),r.addClass("_right"),i(z)&&(r.removeClass("_right"),r.addClass("_top"),i(z)&&(r.removeClass("_top"),r.addClass("_left")))));break;case"bottom":i(z)&&(r.removeClass("_bottom"),r.addClass("_left"),i(z)&&(r.removeClass("_left"),r.addClass("_top"),i(z)&&(r.removeClass("_top"),r.addClass("_right"),i(z)&&(r.removeClass("_right"),r.addClass("_bottom")))));break;case"right":i(z)&&(r.removeClass("_right"),r.addClass("_top"),i(z)&&(r.removeClass("_top"),r.addClass("_left"),i(z)&&(r.removeClass("_left"),r.addClass("_bottom"),i(z)&&(r.removeClass("_bottom"),r.addClass("_right")))));break;default:throw new Error("Position not supported")}if(p.tooltipAppendToBody){var d,e,g,j,k,l=f(A[0]),m=f(C[0]),n=f(z[0]),o=z[0].getBoundingClientRect(),q=a.copy(z),s=0,t=l.length,u=0,v=m.length,w=0,x=n.length,y={},B={},D={};for(z.removeClass("_hidden"),q.removeClass("_hidden"),q.data("_tooltip-parent",r),h(r);t>s;s+=1)d=l[s],d&&l.getPropertyValue(d)&&(y[d]=l.getPropertyValue(d));for(;v>u;u+=1)d=m[u],d&&m.getPropertyValue(d)&&(D[d]=m.getPropertyValue(d));for(;x>w;w+=1)d=n[w],d&&"position"!==d&&"display"!==d&&"opacity"!==d&&"z-index"!==d&&"bottom"!==d&&"height"!==d&&"left"!==d&&"right"!==d&&"top"!==d&&"width"!==d&&n.getPropertyValue(d)&&(B[d]=n.getPropertyValue(d));e=b.parseInt(n.getPropertyValue("padding-top"),10),g=b.parseInt(n.getPropertyValue("padding-bottom"),10),j=b.parseInt(n.getPropertyValue("padding-left"),10),k=b.parseInt(n.getPropertyValue("padding-right"),10),B.top=o.top+b.scrollY+"px",B.left=o.left+b.scrollX+"px",B.height=o.height-(e+g)+"px",B.width=o.width-(j+k)+"px",q.css(B),q.children().css(y),q.children().next().css(D),c&&"true"!==p.tooltipHidden&&(q.addClass("_exradicated-tooltip"),a.element(b.document.body).append(q))}else z.removeClass("_hidden"),c&&"true"!==p.tooltipHidden&&r.addClass("active")},G=function(){p.tooltipAppendToBody?h(r):r.removeClass("active")},H=function da(a){var b,c=a.parent();a[0]&&(a[0].scrollHeight>a[0].clientHeight||a[0].scrollWidth>a[0].clientWidth)&&a.on("scroll",function(){var a=this;b&&l.cancel(b),b=l(function(){var b=g(r),c=r[0].getBoundingClientRect(),d=a.getBoundingClientRect();c.topd.bottom||c.leftd.right?h(r):b&&F(!0)})}),c&&c.length&&da(c)},I=function(a){a?(r.removeClass("_force-hidden"),A.empty(),A.append(B),A.append(a),l(function(){F()})):(A.empty(),r.addClass("_force-hidden"))},J=function(a){a?j.get(a).then(function(a){a&&a.data&&(r.removeClass("_force-hidden"),A.empty(),A.append(B),A.append(k(a.data)(o)),l(function(){F()}))}):(A.empty(),r.addClass("_force-hidden"))},K=function(a){a&&(t&&r.removeAttr("_"+t),r.addClass("_"+a),t=a)},L=function(a){a&&(u&&r.off(u),r.on(a,F),u=a)},M=function(a){a&&(v&&r.off(v),r.on(a,G),v=a)},N=function(a){a&&(s&&z.removeClass(s),z.addClass(a),s=a)},O=function(){"boolean"!=typeof p.tooltipSmart&&(p.tooltipSmart="true"===p.tooltipSmart)},P=function(a){var b="true"===a;b?(B.on("click",G),B.css("display","block")):(B.off("click"),B.css("display","none"))},Q=function(b){if(b){var c,d=m(b,{$scope:o}),e=o.$new(!1,o),f=b.indexOf("as");f>=0?(c=b.substr(f+3),e[c]=d):a.extend(e,d),A.replaceWith(k(A)(e)),_()}},R=function(a){a&&(w&&A.removeClass("_"+w),A.addClass("_"+a),w=a)},S=function(a){a&&(x&&r.removeClass("_"+x),r.addClass("_"+a),x=a)},T=p.$observe("tooltipTemplate",I),U=p.$observe("tooltipTemplateUrl",J),V=p.$observe("tooltipSide",K),W=p.$observe("tooltipShowTrigger",L),X=p.$observe("tooltipHideTrigger",M),Y=p.$observe("tooltipClass",N),Z=p.$observe("tooltipSmart",O),$=p.$observe("tooltipCloseButton",P),_=p.$observe("tooltipController",Q),aa=p.$observe("tooltipSize",R),ba=p.$observe("tooltipSpeed",S),ca=o.$watch(D,E);B.attr({id:"close-button"}),B.html("×"),z.addClass("_hidden"),A.append(B),A.append(p.tooltipTemplate),z.append(A),z.append(C),y.append(c),r.attr(q),r.addClass("tooltips"),r.append(y),r.append(z),n.after(r),p.tooltipAppendToBody&&(d.add(function(){H(r)}),H(r)),d.add(function(){E(),F()}),l(function(){F(),z.removeClass("_hidden"),r.addClass("_ready")}),o.$on("$destroy",function(){T(),U(),V(),W(),X(),Y(),Z(),$(),aa(),ba(),ca(),c.off(p.tooltipShowTrigger+" "+p.tooltipHideTrigger)})})};return{restrict:"A",transclude:"element",priority:1,terminal:!0,link:p}}];a.module("720kb.tooltips",[]).provider(c+"Conf",j).directive(c,k)}(angular,window); -------------------------------------------------------------------------------- /dist/ngWizard.js: -------------------------------------------------------------------------------- 1 | angular.module("ngWizard", [ "720kb.tooltips", "ngAnimate", "ngWizardTemplates" ]).directive("wizard", [ "$window", "$q", function($window, $q) { 2 | "use strict"; 3 | return { 4 | restrict: "E", 5 | transclude: true, 6 | scope: { 7 | currentStepNumber: "=", 8 | submit: "&" 9 | }, 10 | templateUrl: "ngWizardTemplate.html", 11 | controller: function($scope, wizardConfigProvider) { 12 | $scope.prevString = wizardConfigProvider.prevString; 13 | $scope.nextString = wizardConfigProvider.nextString; 14 | $scope.submitString = wizardConfigProvider.submitString; 15 | $scope.currentStepNumber = $scope.currentStepNumber || 0; 16 | $scope.getCurrentStep = function() { 17 | return $scope.steps[$scope.currentStepNumber]; 18 | }; 19 | this.getCurrentStep = $scope.getCurrentStep; 20 | $scope.goToStepByReference = function(step) { 21 | var stepNumber = $scope.steps.indexOf(step); 22 | return $scope.goToStep(stepNumber); 23 | }; 24 | var isValidStepNumber = function(stepNumber) { 25 | return stepNumber < $scope.steps.length && stepNumber >= 0; 26 | }; 27 | $scope.canGoToStep = function(stepNumber) { 28 | if (!isValidStepNumber(stepNumber)) { 29 | return false; 30 | } 31 | var newStep = $scope.steps[stepNumber]; 32 | return $scope.getStepState(newStep) != $scope.stepStatesEnum.disabled; 33 | }; 34 | $scope.goToStep = function(stepNumber) { 35 | if ($scope.canGoToStep(stepNumber)) { 36 | $scope.currentStepNumber = stepNumber; 37 | return true; 38 | } 39 | return false; 40 | }; 41 | $scope.getStepState = function(step) { 42 | if (step.requiredStepNumber && isValidStepNumber(step.requiredStepNumber) && $scope.getStepState($scope.steps[step.requiredStepNumber]) != $scope.stepStatesEnum.complete) { 43 | return $scope.stepStatesEnum.disabled; 44 | } else if (step.stepForm && step.stepForm.$valid) { 45 | return $scope.stepStatesEnum.complete; 46 | } else return $scope.stepStatesEnum.ready; 47 | }; 48 | $scope.stepStatesEnum = { 49 | disabled: 0, 50 | ready: 1, 51 | complete: 2 52 | }; 53 | $scope.goToNext = function() { 54 | $scope.goToStep($scope.currentStepNumber + 1); 55 | }; 56 | $scope.hasNext = function() { 57 | return $scope.steps.length > $scope.currentStepNumber + 1 && $scope.getStepState($scope.steps[$scope.currentStepNumber + 1]) != $scope.stepStatesEnum.disabled; 58 | }; 59 | $scope.goToPrevious = function() { 60 | $scope.goToStep($scope.currentStepNumber - 1); 61 | }; 62 | $scope.hasPrevious = function() { 63 | return $scope.currentStepNumber > 0; 64 | }; 65 | $scope.getProgressPercentage = function() { 66 | var completeSteps = $scope.steps.filter(function(step) { 67 | return $scope.getStepState(step) == $scope.stepStatesEnum.complete; 68 | }); 69 | return completeSteps.length / $scope.steps.length * 100; 70 | }; 71 | $scope.steps = []; 72 | this.registerStep = function(stepScope) { 73 | $scope.steps.push(stepScope); 74 | }; 75 | this.unregisterStep = function(stepScope) { 76 | var index = $scope.steps.indexOf(stepScope); 77 | if (index >= 0) { 78 | $scope.steps.splice(index, 1); 79 | } 80 | }; 81 | $scope.isSubmittable = function() { 82 | return $scope.steps.every(function(step) { 83 | return $scope.getStepState(step) == $scope.stepStatesEnum.complete; 84 | }); 85 | }; 86 | $scope.submitting = false; 87 | $scope.onSubmitClicked = function() { 88 | $scope.submitting = true; 89 | $q.when($scope.submit()).then(function() { 90 | $scope.submitting = false; 91 | }); 92 | }; 93 | $scope.$watch("currentStepNumber", function(val, oldVal) { 94 | if (val != oldVal) { 95 | if (!$scope.canGoToStep(val)) { 96 | if (oldVal && $scope.canGoToStep(oldVal)) { 97 | $scope.currentStepNumber = oldVal; 98 | } else $scope.currentStepNumber = 0; 99 | } else { 100 | $scope.getCurrentStep().entered(); 101 | } 102 | } 103 | }); 104 | $scope.$watch("steps.length", function() { 105 | if (!$scope.getCurrentStep()) { 106 | $scope.currentStepNumber = 0; 107 | } 108 | }, true); 109 | } 110 | }; 111 | } ]).directive("wizardStep", function() { 112 | return { 113 | require: "^wizard", 114 | restrict: "E", 115 | transclude: true, 116 | scope: { 117 | title: "@", 118 | requiredStepNumber: "@", 119 | entered: "&", 120 | animation: "@" 121 | }, 122 | template: "", 123 | link: function($scope, element, attrs, wizardCtrl) { 124 | wizardCtrl.registerStep($scope); 125 | $scope.isActive = function() { 126 | return $scope == wizardCtrl.getCurrentStep(); 127 | }; 128 | $scope.$on("$destroy", function() { 129 | wizardCtrl.unregisterStep($scope); 130 | }); 131 | } 132 | }; 133 | }).provider("wizardConfigProvider", function() { 134 | this.nextString = "Next"; 135 | this.prevString = "Previous"; 136 | this.submitString = "Submit"; 137 | this.$get = function() { 138 | return this; 139 | }; 140 | }); 141 | 142 | angular.module("ngWizardTemplates", [ "ngWizardTemplate.html" ]); 143 | 144 | angular.module("ngWizardTemplate.html", []).run([ "$templateCache", function($templateCache) { 145 | $templateCache.put("ngWizardTemplate.html", '
\n' + '
\n' + ' \n" + "
\n" + '
\n' + ' \n" + '
\n' + "
\n" + '
\n' + '
\n' + ' \n' + "
\n" + "
\n" + "
\n" + ""); 146 | } ]); 147 | 148 | (function withAngular(angular, window) { 149 | "use strict"; 150 | var directiveName = "tooltips", resizeObserver = function resizeObserver() { 151 | var callbacks = [], lastTime = 0, runCallbacks = function runCallbacks(currentTime) { 152 | if (currentTime - lastTime >= 15) { 153 | callbacks.forEach(function iterator(callback) { 154 | callback(); 155 | }); 156 | lastTime = currentTime; 157 | } else { 158 | window.console.log("Skipped!"); 159 | } 160 | }, resize = function resize() { 161 | window.requestAnimationFrame(runCallbacks); 162 | }, addCallback = function addCallback(callback) { 163 | if (callback) { 164 | callbacks.push(callback); 165 | } 166 | }; 167 | return { 168 | add: function add(callback) { 169 | if (!callbacks.length) { 170 | window.addEventListener("resize", resize); 171 | } 172 | addCallback(callback); 173 | } 174 | }; 175 | }(), getAttributesToAdd = function getAttributesToAdd(element) { 176 | var attributesToAdd = {}; 177 | element.removeAttr(directiveName); 178 | if (element.attr("tooltip-template") !== undefined) { 179 | attributesToAdd["tooltip-template"] = element.attr("tooltip-template"); 180 | element.removeAttr("tooltip-template"); 181 | } 182 | if (element.attr("tooltip-template-url") !== undefined) { 183 | attributesToAdd["tooltip-template-url"] = element.attr("tooltip-template-url"); 184 | element.removeAttr("tooltip-template-url"); 185 | } 186 | if (element.attr("tooltip-controller") !== undefined) { 187 | attributesToAdd["tooltip-controller"] = element.attr("tooltip-controller"); 188 | element.removeAttr("tooltip-controller"); 189 | } 190 | if (element.attr("tooltip-side") !== undefined) { 191 | attributesToAdd["tooltip-side"] = element.attr("tooltip-side"); 192 | element.removeAttr("tooltip-side"); 193 | } 194 | if (element.attr("tooltip-show-trigger") !== undefined) { 195 | attributesToAdd["tooltip-show-trigger"] = element.attr("tooltip-show-trigger"); 196 | element.removeAttr("tooltip-show-trigger"); 197 | } 198 | if (element.attr("tooltip-hide-trigger") !== undefined) { 199 | attributesToAdd["tooltip-hide-trigger"] = element.attr("tooltip-hide-trigger"); 200 | element.removeAttr("tooltip-hide-trigger"); 201 | } 202 | if (element.attr("tooltip-smart") !== undefined) { 203 | attributesToAdd["tooltip-smart"] = element.attr("tooltip-smart"); 204 | element.removeAttr("tooltip-smart"); 205 | } 206 | if (element.attr("tooltip-class") !== undefined) { 207 | attributesToAdd["tooltip-class"] = element.attr("tooltip-class"); 208 | element.removeAttr("tooltip-class"); 209 | } 210 | if (element.attr("tooltip-close-button") !== undefined) { 211 | attributesToAdd["tooltip-close-button"] = element.attr("tooltip-close-button"); 212 | element.removeAttr("tooltip-close-button"); 213 | } 214 | if (element.attr("tooltip-size") !== undefined) { 215 | attributesToAdd["tooltip-size"] = element.attr("tooltip-size"); 216 | element.removeAttr("tooltip-size"); 217 | } 218 | if (element.attr("tooltip-speed") !== undefined) { 219 | attributesToAdd["tooltip-speed"] = element.attr("tooltip-speed"); 220 | element.removeAttr("tooltip-speed"); 221 | } 222 | return attributesToAdd; 223 | }, getStyle = function getStyle(anElement) { 224 | if (window.getComputedStyle) { 225 | return window.getComputedStyle(anElement, ""); 226 | } else if (anElement.currentStyle) { 227 | return anElement.currentStyle; 228 | } 229 | }, getAppendedTip = function getAppendedTip(theTooltipElement) { 230 | var tipsInBody = window.document.querySelectorAll("._exradicated-tooltip"), aTipInBody, tipsInBodyIndex = 0, tipsInBodyLength = tipsInBody.length, angularizedElement; 231 | for (;tipsInBodyIndex < tipsInBodyLength; tipsInBodyIndex += 1) { 232 | aTipInBody = tipsInBody.item(tipsInBodyIndex); 233 | if (aTipInBody) { 234 | angularizedElement = angular.element(aTipInBody); 235 | if (angularizedElement.data("_tooltip-parent") && angularizedElement.data("_tooltip-parent") === theTooltipElement) { 236 | return angularizedElement; 237 | } 238 | } 239 | } 240 | }, removeAppendedTip = function removeAppendedTip(theTooltipElement) { 241 | var tipElement = getAppendedTip(theTooltipElement); 242 | if (tipElement) { 243 | tipElement.remove(); 244 | } 245 | }, isOutOfPage = function isOutOfPage(theTipElement) { 246 | if (theTipElement) { 247 | var squarePosition = theTipElement[0].getBoundingClientRect(); 248 | if (squarePosition.top < 0 || squarePosition.top > window.document.body.offsetHeight || squarePosition.left < 0 || squarePosition.left > window.document.body.offsetWidth || squarePosition.bottom < 0 || squarePosition.bottom > window.document.body.offsetHeight || squarePosition.right < 0 || squarePosition.right > window.document.body.offsetWidth) { 249 | theTipElement.css({ 250 | top: "", 251 | left: "", 252 | bottom: "", 253 | right: "" 254 | }); 255 | return true; 256 | } 257 | return false; 258 | } 259 | throw new Error("You must provide a position"); 260 | }, tooltipConfigurationProvider = function tooltipConfigurationProvider() { 261 | var tooltipConfiguration = { 262 | side: "top", 263 | showTrigger: "mouseover", 264 | hideTrigger: "mouseleave", 265 | "class": "", 266 | smart: false, 267 | closeButton: false, 268 | size: "", 269 | speed: "steady" 270 | }; 271 | return { 272 | configure: function configure(configuration) { 273 | var configurationKeys = Object.keys(tooltipConfiguration), configurationIndex = 0, aConfigurationKey; 274 | if (configuration) { 275 | for (;configurationIndex < configurationKeys.length; configurationIndex += 1) { 276 | aConfigurationKey = configurationKeys[configurationIndex]; 277 | if (aConfigurationKey && configuration[aConfigurationKey]) { 278 | tooltipConfiguration[aConfigurationKey] = configuration[aConfigurationKey]; 279 | } 280 | } 281 | } 282 | }, 283 | $get: function instantiateProvider() { 284 | return tooltipConfiguration; 285 | } 286 | }; 287 | }, tooltipDirective = [ "$log", "$http", "$compile", "$timeout", "$controller", "$injector", "tooltipsConf", function tooltipDirective($log, $http, $compile, $timeout, $controller, $injector, tooltipsConf) { 288 | var linkingFunction = function linkingFunction($scope, $element, $attrs, $controllerDirective, $transcludeFunc) { 289 | if ($attrs.tooltipTemplate && $attrs.tooltipTemplateUrl) { 290 | throw new Error("You can not define tooltip-template and tooltip-template-url together"); 291 | } 292 | if (!($attrs.tooltipTemplateUrl || $attrs.tooltipTemplate) && $attrs.tooltipController) { 293 | throw new Error("You can not have a controller without a template or templateUrl defined"); 294 | } 295 | var oldTooltipSide = "_" + tooltipsConf.side, oldTooltipShowTrigger = tooltipsConf.showTrigger, oldTooltipHideTrigger = tooltipsConf.hideTrigger, oldTooltipClass, oldSize = tooltipsConf.size, oldSpeed = "_" + tooltipsConf.speed; 296 | $attrs.tooltipSide = $attrs.tooltipSide || tooltipsConf.side; 297 | $attrs.tooltipShowTrigger = $attrs.tooltipShowTrigger || tooltipsConf.showTrigger; 298 | $attrs.tooltipHideTrigger = $attrs.tooltipHideTrigger || tooltipsConf.hideTrigger; 299 | $attrs.tooltipClass = $attrs.tooltipClass || tooltipsConf.class; 300 | $attrs.tooltipSmart = $attrs.tooltipSmart === "true" || tooltipsConf.smart; 301 | $attrs.tooltipCloseButton = $attrs.tooltipCloseButton || tooltipsConf.closeButton.toString(); 302 | $attrs.tooltipSize = $attrs.tooltipSize || tooltipsConf.size; 303 | $attrs.tooltipSpeed = $attrs.tooltipSpeed || tooltipsConf.speed; 304 | $attrs.tooltipAppendToBody = $attrs.tooltipAppendToBody === "true"; 305 | $transcludeFunc($scope, function onTransclusionDone(element, scope) { 306 | var attributes = getAttributesToAdd(element), tooltipElement = angular.element(window.document.createElement("tooltip")), tipContElement = angular.element(window.document.createElement("tip-cont")), tipElement = angular.element(window.document.createElement("tip")), tipTipElement = angular.element(window.document.createElement("tip-tip")), closeButtonElement = angular.element(window.document.createElement("span")), tipArrowElement = angular.element(window.document.createElement("tip-arrow")), whenActivateMultilineCalculation = function whenActivateMultilineCalculation() { 307 | return tipContElement.html(); 308 | }, calculateIfMultiLine = function calculateIfMultiLine(newValue) { 309 | if (newValue !== undefined && tipContElement[0].getClientRects().length > 1) { 310 | tooltipElement.addClass("_multiline"); 311 | } else { 312 | tooltipElement.removeClass("_multiline"); 313 | } 314 | }, onTooltipShow = function onTooltipShow(event) { 315 | tipElement.addClass("_hidden"); 316 | if ($attrs.tooltipSmart) { 317 | switch ($attrs.tooltipSide) { 318 | case "top": 319 | { 320 | if (isOutOfPage(tipElement)) { 321 | tooltipElement.removeClass("_top"); 322 | tooltipElement.addClass("_left"); 323 | if (isOutOfPage(tipElement)) { 324 | tooltipElement.removeClass("_left"); 325 | tooltipElement.addClass("_bottom"); 326 | if (isOutOfPage(tipElement)) { 327 | tooltipElement.removeClass("_bottom"); 328 | tooltipElement.addClass("_right"); 329 | if (isOutOfPage(tipElement)) { 330 | tooltipElement.removeClass("_right"); 331 | tooltipElement.addClass("_top"); 332 | } 333 | } 334 | } 335 | } 336 | break; 337 | } 338 | 339 | case "left": 340 | { 341 | if (isOutOfPage(tipElement)) { 342 | tooltipElement.removeClass("_left"); 343 | tooltipElement.addClass("_bottom"); 344 | if (isOutOfPage(tipElement)) { 345 | tooltipElement.removeClass("_bottom"); 346 | tooltipElement.addClass("_right"); 347 | if (isOutOfPage(tipElement)) { 348 | tooltipElement.removeClass("_right"); 349 | tooltipElement.addClass("_top"); 350 | if (isOutOfPage(tipElement)) { 351 | tooltipElement.removeClass("_top"); 352 | tooltipElement.addClass("_left"); 353 | } 354 | } 355 | } 356 | } 357 | break; 358 | } 359 | 360 | case "bottom": 361 | { 362 | if (isOutOfPage(tipElement)) { 363 | tooltipElement.removeClass("_bottom"); 364 | tooltipElement.addClass("_left"); 365 | if (isOutOfPage(tipElement)) { 366 | tooltipElement.removeClass("_left"); 367 | tooltipElement.addClass("_top"); 368 | if (isOutOfPage(tipElement)) { 369 | tooltipElement.removeClass("_top"); 370 | tooltipElement.addClass("_right"); 371 | if (isOutOfPage(tipElement)) { 372 | tooltipElement.removeClass("_right"); 373 | tooltipElement.addClass("_bottom"); 374 | } 375 | } 376 | } 377 | } 378 | break; 379 | } 380 | 381 | case "right": 382 | { 383 | if (isOutOfPage(tipElement)) { 384 | tooltipElement.removeClass("_right"); 385 | tooltipElement.addClass("_top"); 386 | if (isOutOfPage(tipElement)) { 387 | tooltipElement.removeClass("_top"); 388 | tooltipElement.addClass("_left"); 389 | if (isOutOfPage(tipElement)) { 390 | tooltipElement.removeClass("_left"); 391 | tooltipElement.addClass("_bottom"); 392 | if (isOutOfPage(tipElement)) { 393 | tooltipElement.removeClass("_bottom"); 394 | tooltipElement.addClass("_right"); 395 | } 396 | } 397 | } 398 | } 399 | break; 400 | } 401 | 402 | default: 403 | { 404 | throw new Error("Position not supported"); 405 | } 406 | } 407 | } 408 | if ($attrs.tooltipAppendToBody) { 409 | var tipTipElementStyle = getStyle(tipTipElement[0]), tipArrowElementStyle = getStyle(tipArrowElement[0]), tipElementStyle = getStyle(tipElement[0]), tipElementBoundingClientRect = tipElement[0].getBoundingClientRect(), exradicatedTipElement = angular.copy(tipElement), tipTipStyleIndex = 0, tipTipStyleLength = tipTipElementStyle.length, tipArrowStyleIndex = 0, tipArrowStyleLength = tipArrowElementStyle.length, tipStyleIndex = 0, tipStyleLength = tipElementStyle.length, aStyleKey, tipTipCssToSet = {}, tipCssToSet = {}, tipArrowCssToSet = {}, paddingTopValue, paddingBottomValue, paddingLeftValue, paddingRightValue; 410 | tipElement.removeClass("_hidden"); 411 | exradicatedTipElement.removeClass("_hidden"); 412 | exradicatedTipElement.data("_tooltip-parent", tooltipElement); 413 | removeAppendedTip(tooltipElement); 414 | for (;tipTipStyleIndex < tipTipStyleLength; tipTipStyleIndex += 1) { 415 | aStyleKey = tipTipElementStyle[tipTipStyleIndex]; 416 | if (aStyleKey && tipTipElementStyle.getPropertyValue(aStyleKey)) { 417 | tipTipCssToSet[aStyleKey] = tipTipElementStyle.getPropertyValue(aStyleKey); 418 | } 419 | } 420 | for (;tipArrowStyleIndex < tipArrowStyleLength; tipArrowStyleIndex += 1) { 421 | aStyleKey = tipArrowElementStyle[tipArrowStyleIndex]; 422 | if (aStyleKey && tipArrowElementStyle.getPropertyValue(aStyleKey)) { 423 | tipArrowCssToSet[aStyleKey] = tipArrowElementStyle.getPropertyValue(aStyleKey); 424 | } 425 | } 426 | for (;tipStyleIndex < tipStyleLength; tipStyleIndex += 1) { 427 | aStyleKey = tipElementStyle[tipStyleIndex]; 428 | if (aStyleKey && aStyleKey !== "position" && aStyleKey !== "display" && aStyleKey !== "opacity" && aStyleKey !== "z-index" && aStyleKey !== "bottom" && aStyleKey !== "height" && aStyleKey !== "left" && aStyleKey !== "right" && aStyleKey !== "top" && aStyleKey !== "width" && tipElementStyle.getPropertyValue(aStyleKey)) { 429 | tipCssToSet[aStyleKey] = tipElementStyle.getPropertyValue(aStyleKey); 430 | } 431 | } 432 | paddingTopValue = window.parseInt(tipElementStyle.getPropertyValue("padding-top"), 10); 433 | paddingBottomValue = window.parseInt(tipElementStyle.getPropertyValue("padding-bottom"), 10); 434 | paddingLeftValue = window.parseInt(tipElementStyle.getPropertyValue("padding-left"), 10); 435 | paddingRightValue = window.parseInt(tipElementStyle.getPropertyValue("padding-right"), 10); 436 | tipCssToSet.top = tipElementBoundingClientRect.top + window.scrollY + "px"; 437 | tipCssToSet.left = tipElementBoundingClientRect.left + window.scrollX + "px"; 438 | tipCssToSet.height = tipElementBoundingClientRect.height - (paddingTopValue + paddingBottomValue) + "px"; 439 | tipCssToSet.width = tipElementBoundingClientRect.width - (paddingLeftValue + paddingRightValue) + "px"; 440 | exradicatedTipElement.css(tipCssToSet); 441 | exradicatedTipElement.children().css(tipTipCssToSet); 442 | exradicatedTipElement.children().next().css(tipArrowCssToSet); 443 | if (event && $attrs.tooltipHidden !== "true") { 444 | exradicatedTipElement.addClass("_exradicated-tooltip"); 445 | angular.element(window.document.body).append(exradicatedTipElement); 446 | } 447 | } else { 448 | tipElement.removeClass("_hidden"); 449 | if (event && $attrs.tooltipHidden !== "true") { 450 | tooltipElement.addClass("active"); 451 | } 452 | } 453 | }, onTooltipHide = function onTooltipHide() { 454 | if ($attrs.tooltipAppendToBody) { 455 | removeAppendedTip(tooltipElement); 456 | } else { 457 | tooltipElement.removeClass("active"); 458 | } 459 | }, registerOnScrollFrom = function registerOnScrollFrom(theElement) { 460 | var parentElement = theElement.parent(), timer; 461 | if (theElement[0] && (theElement[0].scrollHeight > theElement[0].clientHeight || theElement[0].scrollWidth > theElement[0].clientWidth)) { 462 | theElement.on("scroll", function onScroll() { 463 | var that = this; 464 | if (timer) { 465 | $timeout.cancel(timer); 466 | } 467 | timer = $timeout(function doLater() { 468 | var theTipElement = getAppendedTip(tooltipElement), tooltipBoundingRect = tooltipElement[0].getBoundingClientRect(), thatBoundingRect = that.getBoundingClientRect(); 469 | if (tooltipBoundingRect.top < thatBoundingRect.top || tooltipBoundingRect.bottom > thatBoundingRect.bottom || tooltipBoundingRect.left < thatBoundingRect.left || tooltipBoundingRect.right > thatBoundingRect.right) { 470 | removeAppendedTip(tooltipElement); 471 | } else if (theTipElement) { 472 | onTooltipShow(true); 473 | } 474 | }); 475 | }); 476 | } 477 | if (parentElement && parentElement.length) { 478 | registerOnScrollFrom(parentElement); 479 | } 480 | }, onTooltipTemplateChange = function onTooltipTemplateChange(newValue) { 481 | if (newValue) { 482 | tooltipElement.removeClass("_force-hidden"); 483 | tipTipElement.empty(); 484 | tipTipElement.append(closeButtonElement); 485 | tipTipElement.append(newValue); 486 | $timeout(function doLaterShow() { 487 | onTooltipShow(); 488 | }); 489 | } else { 490 | tipTipElement.empty(); 491 | tooltipElement.addClass("_force-hidden"); 492 | } 493 | }, onTooltipTemplateUrlChange = function onTooltipTemplateUrlChange(newValue) { 494 | if (newValue) { 495 | $http.get(newValue).then(function onResponse(response) { 496 | if (response && response.data) { 497 | tooltipElement.removeClass("_force-hidden"); 498 | tipTipElement.empty(); 499 | tipTipElement.append(closeButtonElement); 500 | tipTipElement.append($compile(response.data)(scope)); 501 | $timeout(function doLater() { 502 | onTooltipShow(); 503 | }); 504 | } 505 | }); 506 | } else { 507 | tipTipElement.empty(); 508 | tooltipElement.addClass("_force-hidden"); 509 | } 510 | }, onTooltipSideChange = function onTooltipSideChange(newValue) { 511 | if (newValue) { 512 | if (oldTooltipSide) { 513 | tooltipElement.removeAttr("_" + oldTooltipSide); 514 | } 515 | tooltipElement.addClass("_" + newValue); 516 | oldTooltipSide = newValue; 517 | } 518 | }, onTooltipShowTrigger = function onTooltipShowTrigger(newValue) { 519 | if (newValue) { 520 | if (oldTooltipShowTrigger) { 521 | tooltipElement.off(oldTooltipShowTrigger); 522 | } 523 | tooltipElement.on(newValue, onTooltipShow); 524 | oldTooltipShowTrigger = newValue; 525 | } 526 | }, onTooltipHideTrigger = function onTooltipHideTrigger(newValue) { 527 | if (newValue) { 528 | if (oldTooltipHideTrigger) { 529 | tooltipElement.off(oldTooltipHideTrigger); 530 | } 531 | tooltipElement.on(newValue, onTooltipHide); 532 | oldTooltipHideTrigger = newValue; 533 | } 534 | }, onTooltipClassChange = function onTooltipClassChange(newValue) { 535 | if (newValue) { 536 | if (oldTooltipClass) { 537 | tipElement.removeClass(oldTooltipClass); 538 | } 539 | tipElement.addClass(newValue); 540 | oldTooltipClass = newValue; 541 | } 542 | }, onTooltipSmartChange = function onTooltipSmartChange() { 543 | if (typeof $attrs.tooltipSmart !== "boolean") { 544 | $attrs.tooltipSmart = $attrs.tooltipSmart === "true"; 545 | } 546 | }, onTooltipCloseButtonChange = function onTooltipCloseButtonChange(newValue) { 547 | var enableButton = newValue === "true"; 548 | if (enableButton) { 549 | closeButtonElement.on("click", onTooltipHide); 550 | closeButtonElement.css("display", "block"); 551 | } else { 552 | closeButtonElement.off("click"); 553 | closeButtonElement.css("display", "none"); 554 | } 555 | }, onTooltipTemplateControllerChange = function onTooltipTemplateControllerChange(newValue) { 556 | if (newValue) { 557 | var tipController = $controller(newValue, { 558 | $scope: scope 559 | }), newScope = scope.$new(false, scope), indexOfAs = newValue.indexOf("as"), controllerName; 560 | if (indexOfAs >= 0) { 561 | controllerName = newValue.substr(indexOfAs + 3); 562 | newScope[controllerName] = tipController; 563 | } else { 564 | angular.extend(newScope, tipController); 565 | } 566 | tipTipElement.replaceWith($compile(tipTipElement)(newScope)); 567 | unregisterOnTooltipControllerChange(); 568 | } 569 | }, onTooltipSizeChange = function onTooltipSizeChange(newValue) { 570 | if (newValue) { 571 | if (oldSize) { 572 | tipTipElement.removeClass("_" + oldSize); 573 | } 574 | tipTipElement.addClass("_" + newValue); 575 | oldSize = newValue; 576 | } 577 | }, onTooltipSpeedChange = function onTooltipSpeedChange(newValue) { 578 | if (newValue) { 579 | if (oldSpeed) { 580 | tooltipElement.removeClass("_" + oldSpeed); 581 | } 582 | tooltipElement.addClass("_" + newValue); 583 | oldSpeed = newValue; 584 | } 585 | }, unregisterOnTooltipTemplateChange = $attrs.$observe("tooltipTemplate", onTooltipTemplateChange), unregisterOnTooltipTemplateUrlChange = $attrs.$observe("tooltipTemplateUrl", onTooltipTemplateUrlChange), unregisterOnTooltipSideChangeObserver = $attrs.$observe("tooltipSide", onTooltipSideChange), unregisterOnTooltipShowTrigger = $attrs.$observe("tooltipShowTrigger", onTooltipShowTrigger), unregisterOnTooltipHideTrigger = $attrs.$observe("tooltipHideTrigger", onTooltipHideTrigger), unregisterOnTooltipClassChange = $attrs.$observe("tooltipClass", onTooltipClassChange), unregisterOnTooltipSmartChange = $attrs.$observe("tooltipSmart", onTooltipSmartChange), unregisterOnTooltipCloseButtonChange = $attrs.$observe("tooltipCloseButton", onTooltipCloseButtonChange), unregisterOnTooltipControllerChange = $attrs.$observe("tooltipController", onTooltipTemplateControllerChange), unregisterOnTooltipSizeChange = $attrs.$observe("tooltipSize", onTooltipSizeChange), unregisterOnTooltipSpeedChange = $attrs.$observe("tooltipSpeed", onTooltipSpeedChange), unregisterTipContentChangeWatcher = scope.$watch(whenActivateMultilineCalculation, calculateIfMultiLine); 586 | closeButtonElement.attr({ 587 | id: "close-button" 588 | }); 589 | closeButtonElement.html("×"); 590 | tipElement.addClass("_hidden"); 591 | tipTipElement.append(closeButtonElement); 592 | tipTipElement.append($attrs.tooltipTemplate); 593 | tipElement.append(tipTipElement); 594 | tipElement.append(tipArrowElement); 595 | tipContElement.append(element); 596 | tooltipElement.attr(attributes); 597 | tooltipElement.addClass("tooltips"); 598 | tooltipElement.append(tipContElement); 599 | tooltipElement.append(tipElement); 600 | $element.after(tooltipElement); 601 | if ($attrs.tooltipAppendToBody) { 602 | resizeObserver.add(function onResize() { 603 | registerOnScrollFrom(tooltipElement); 604 | }); 605 | registerOnScrollFrom(tooltipElement); 606 | } 607 | resizeObserver.add(function registerResize() { 608 | calculateIfMultiLine(); 609 | onTooltipShow(); 610 | }); 611 | $timeout(function doLater() { 612 | onTooltipShow(); 613 | tipElement.removeClass("_hidden"); 614 | tooltipElement.addClass("_ready"); 615 | }); 616 | scope.$on("$destroy", function unregisterListeners() { 617 | unregisterOnTooltipTemplateChange(); 618 | unregisterOnTooltipTemplateUrlChange(); 619 | unregisterOnTooltipSideChangeObserver(); 620 | unregisterOnTooltipShowTrigger(); 621 | unregisterOnTooltipHideTrigger(); 622 | unregisterOnTooltipClassChange(); 623 | unregisterOnTooltipSmartChange(); 624 | unregisterOnTooltipCloseButtonChange(); 625 | unregisterOnTooltipSizeChange(); 626 | unregisterOnTooltipSpeedChange(); 627 | unregisterTipContentChangeWatcher(); 628 | element.off($attrs.tooltipShowTrigger + " " + $attrs.tooltipHideTrigger); 629 | }); 630 | }); 631 | }; 632 | return { 633 | restrict: "A", 634 | transclude: "element", 635 | priority: 1, 636 | terminal: true, 637 | link: linkingFunction 638 | }; 639 | } ]; 640 | angular.module("720kb.tooltips", []).provider(directiveName + "Conf", tooltipConfigurationProvider).directive(directiveName, tooltipDirective); 641 | })(angular, window); --------------------------------------------------------------------------------