├── src ├── img │ ├── signup.png │ └── confirm_mark.png ├── examples │ ├── App.js │ ├── index.html │ ├── Example.js │ ├── Step3.js │ ├── Step1.js │ └── Step2.js ├── css │ ├── prog-tracker.css │ ├── main.css │ ├── multistep.css │ ├── custom.css │ ├── normalize.css │ └── skeleton.css └── main.js ├── .babelrc ├── .eslintrc ├── .gitignore ├── webpack.config.js ├── README.md~ ├── README.md ├── package.json ├── gulpfile.js ├── docs ├── index.html └── main.js └── dist └── main.js /src/img/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Big-Silver/React-Multistep/HEAD/src/img/signup.png -------------------------------------------------------------------------------- /src/img/confirm_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Big-Silver/React-Multistep/HEAD/src/img/confirm_mark.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-object-rest-spread" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | env: 4 | node: true 5 | 6 | extends: 7 | "airbnb/base" 8 | 9 | parser: 10 | "babel-eslint" 11 | 12 | rules: 13 | func-names: 0 14 | comma-dangle: [2, "never"] 15 | no-console: 0 16 | max-len: [2, 200] 17 | -------------------------------------------------------------------------------- /src/examples/App.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Example from './Example'; 6 | import '../css/skeleton.css' 7 | import '../css/prog-tracker.css' 8 | import '../css/custom.css' 9 | import '../css/normalize.css' 10 | 11 | require('../css/main.css'); 12 | require('../css/multistep.css'); 13 | 14 | ReactDOM.render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # working files 2 | .vim/viminfo 3 | .vim/swaps/* 4 | .vim/undo 5 | 6 | # git credential file 7 | .gitconfig.local 8 | 9 | # no thx 10 | node_modules 11 | coverage 12 | src/examples/dist 13 | npm-debug.log 14 | 15 | # Folder view configuration files 16 | .DS_Store 17 | Desktop.ini 18 | 19 | # Thumbnail cache files 20 | ._* 21 | Thumbs.db 22 | 23 | # Files that might appear on external disks 24 | .Spotlight-V100 25 | .Trashes 26 | 27 | # Compiled Python files 28 | *.pyc 29 | 30 | # fish shell 31 | fishd.* 32 | .config/fish/fish_history 33 | fish/fish_history 34 | 35 | # extra 36 | .extra 37 | 38 | # random 39 | shilocation 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | example: [ 4 | 'webpack-dev-server/client?http://localhost:8080', 5 | './src/examples/App.js', 6 | ] 7 | }, 8 | output: { 9 | path: './src/examples/dist', 10 | filename: './src/examples/dist/bundle.js' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader' 18 | }, 19 | { 20 | test: /\.css$/, 21 | loader: 'style-loader!css-loader' 22 | }, 23 | { 24 | test: /\.json$/, 25 | loader: 'json-loader' 26 | } 27 | ] 28 | }, 29 | resolve: { 30 | extensions: ['', '.js', '.json'] 31 | }, 32 | node: { 33 | net: 'empty', 34 | tls: 'empty', 35 | dns: 'empty' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /README.md~: -------------------------------------------------------------------------------- 1 | # React-MultiStep 2 | 3 | 4 | 5 | ## About 6 | Using Jquery, Html, CSS and Javascript, implement the D & D(drag and drop), clone, resize, rotate and remove. 7 | This Jquery Drag and Drop example is written by [Big Silver]. 8 | ## install and run 9 | 10 | - run `npm install` 11 | - then run `npm run example` 12 | - then go to `http://localhost:8080/webpack-dev-server/src/examples/index.html` in your browser 13 | 14 | 15 | ## tests 16 | - tests are written in the mocha, chai, sinon, enzyme stack 17 | - located in the 'tests' folder and supports es6 18 | - run the `npm run test` command run tests 19 | - run the `npm run test:watch` command run test in watch mode 20 | 21 | ## code test coverage 22 | - test coverage is done via istanbul 23 | - run the `npm run test:coverage` command to generate full coverage report (shown in terminal and as lcov report in coverage directory) 24 | - all code is run against coverage, not just the unit tested modules 25 | - test coverage improvement is currently a work in progress 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-MultiStep 2 | 3 | 4 | 5 | ## About 6 | Using React.js and React-Multistep, I created the SignUp page 7 | This React-MultiStep example is written by [Big Silver]. 8 | 9 | ## install and run 10 | 11 | - Open termianl `git clone https://github.com/Big-Silver/React-Multistep.git react-multistep` 12 | - Move to the project `cd react-multistep` 13 | - Run `npm install` 14 | - Then run `npm run example` 15 | - Then go to `http://localhost:8080/webpack-dev-server/src/examples/index.html` in your browser 16 | 17 | 18 | ## tests 19 | - tests are written in the mocha, chai, sinon, enzyme stack 20 | - located in the 'tests' folder and supports es6 21 | - run the `npm run test` command run tests 22 | - run the `npm run test:watch` command run test in watch mode 23 | 24 | ## code test coverage 25 | - test coverage is done via istanbul 26 | - run the `npm run test:coverage` command to generate full coverage report (shown in terminal and as lcov report in coverage directory) 27 | - all code is run against coverage, not just the unit tested modules 28 | - test coverage improvement is currently a work in progress 29 | 30 | -------------------------------------------------------------------------------- /src/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SignUp 6 | 7 | 8 | 9 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/examples/Example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component, PropTypes } from 'react'; 4 | import StepZilla from '../main' 5 | import Step1 from './Step1' 6 | import Step2 from './Step2' 7 | import Step3 from './Step3' 8 | 9 | export default class Example extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {}; 13 | 14 | this.sampleStore = { 15 | email: '', 16 | password: '', 17 | gender: '', 18 | day: '', 19 | month: '', 20 | year: '', 21 | savedToCloud: false 22 | }; 23 | } 24 | 25 | componentDidMount() {} 26 | 27 | componentWillUnmount() {} 28 | 29 | getStore() { 30 | return this.sampleStore; 31 | } 32 | 33 | updateStore(update) { 34 | this.sampleStore = { 35 | ...this.sampleStore, 36 | ...update, 37 | } 38 | } 39 | 40 | render() { 41 | const steps = 42 | [ 43 | {name: 'Signup', component: (this.getStore())} updateStore={(u) => {this.updateStore(u)}} />}, 44 | {name: 'Signup', component: (this.getStore())} updateStore={(u) => {this.updateStore(u)}} />}, 45 | {name: 'Thank you!', component: (this.getStore())} updateStore={(u) => {this.updateStore(u)}} />} 46 | ] 47 | 48 | return ( 49 |
50 |
51 | 57 |
58 |
59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/css/prog-tracker.css: -------------------------------------------------------------------------------- 1 | ol.progtrckr { 2 | margin: 0; 3 | padding-bottom: 2.2rem; 4 | list-style-type: none; 5 | } 6 | ol.progtrckr li { 7 | display: inline-block; 8 | text-align: center; 9 | line-height: 4.5rem; 10 | padding: 0 0.7rem; 11 | cursor: pointer; 12 | } 13 | ol.progtrckr li span { 14 | padding: 0 1.5rem; 15 | } 16 | @media (max-width: 650px) { 17 | .progtrckr li span { 18 | display: none; 19 | } 20 | } 21 | .progtrckr em { 22 | display: none; 23 | font-weight: 700; 24 | padding-left: 1rem; 25 | } 26 | @media (max-width: 650px) { 27 | .progtrckr em { 28 | display: inline; 29 | } 30 | } 31 | 32 | ol.progtrckr li.progtrckr-todo { 33 | color: silver; 34 | border-bottom: 4px solid silver; 35 | } 36 | ol.progtrckr li.progtrckr-doing { 37 | color: black; 38 | border-bottom: 4px solid #33C3F0; 39 | } 40 | ol.progtrckr li.progtrckr-done { 41 | color: black; 42 | border-bottom: 4px solid #33C3F0; 43 | } 44 | ol.progtrckr li:after { 45 | content: "\00a0\00a0"; 46 | } 47 | ol.progtrckr li:before { 48 | position: relative; 49 | bottom: -3.7rem; 50 | float: left; 51 | left: 50%; 52 | } 53 | ol.progtrckr li.progtrckr-todo:before { 54 | content: "\039F"; 55 | color: silver; 56 | background-color: white; 57 | width: 1.2em; 58 | line-height: 1.4em; 59 | } 60 | ol.progtrckr li.progtrckr-todo:hover:before { 61 | color: #0FA0CE; 62 | } 63 | 64 | ol.progtrckr li.progtrckr-doing:before { 65 | content: "\2022"; 66 | color: white; 67 | background-color: #33C3F0; 68 | width: 1.2em; 69 | line-height: 1.2em; 70 | border-radius: 1.2em; 71 | } 72 | ol.progtrckr li.progtrckr-doing:hover:before { 73 | color: #0FA0CE; 74 | } 75 | 76 | ol.progtrckr li.progtrckr-done:before { 77 | content: "\2713"; 78 | color: white; 79 | background-color: #33C3F0; 80 | width: 1.2em; 81 | line-height: 1.2em; 82 | border-radius: 1.2em; 83 | } 84 | ol.progtrckr li.progtrckr-done:hover:before { 85 | color: #0FA0CE; 86 | } 87 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | ol.progtrckr { 2 | list-style-type: none; 3 | padding: 0; 4 | margin-bottom: 35px; 5 | } 6 | 7 | ol.progtrckr li { 8 | display: inline-block; 9 | text-align: center; 10 | line-height: 4.5rem; 11 | padding: 0 0.7rem; 12 | cursor: pointer; 13 | } 14 | 15 | ol.progtrckr li span { 16 | color: #33C3F0; 17 | padding: 0 1.5rem; 18 | } 19 | 20 | @media (max-width: 650px) { 21 | .progtrckr li span { 22 | display: none; 23 | } 24 | } 25 | .progtrckr em { 26 | display: none; 27 | font-weight: 700; 28 | padding-left: 1rem; 29 | } 30 | 31 | @media (max-width: 650px) { 32 | .progtrckr em { 33 | display: inline; 34 | } 35 | } 36 | 37 | ol.progtrckr li.progtrckr-todo { 38 | color: transparent; 39 | border-bottom: 4px solid silver; 40 | } 41 | 42 | ol.progtrckr li.progtrckr-doing { 43 | color: black; 44 | border-bottom: 4px solid #33C3F0; 45 | } 46 | 47 | ol.progtrckr li.progtrckr-done { 48 | color: black; 49 | border-bottom: 4px solid #33C3F0; 50 | } 51 | 52 | ol.progtrckr li:after { 53 | content: "\00a0\00a0"; 54 | } 55 | 56 | ol.progtrckr li:before { 57 | position: relative; 58 | bottom: -3.7rem; 59 | float: left; 60 | left: 50%; 61 | } 62 | 63 | ol.progtrckr li.progtrckr-todo:before { 64 | content: "\039F"; 65 | color: silver; 66 | background-color: white; 67 | width: 1.2em; 68 | line-height: 1.4em; 69 | } 70 | 71 | ol.progtrckr li.progtrckr-todo:hover:before { 72 | color: #5cb85c; 73 | } 74 | 75 | ol.progtrckr li.progtrckr-doing:before { 76 | content: "\2022"; 77 | color: white; 78 | background-color: #33C3F0; 79 | width: 1.2em; 80 | line-height: 1.2em; 81 | border-radius: 1.2em; 82 | } 83 | 84 | ol.progtrckr li.progtrckr-doing:hover:before { 85 | color: #5cb85c; 86 | } 87 | 88 | ol.progtrckr li.progtrckr-done:before { 89 | content: "\2713"; 90 | color: white; 91 | background-color: #33C3F0; 92 | width: 1.2em; 93 | line-height: 1.2em; 94 | border-radius: 1.2em; 95 | } 96 | 97 | ol.progtrckr li.progtrckr-done:hover:before { 98 | color: #333; 99 | } 100 | -------------------------------------------------------------------------------- /src/css/multistep.css: -------------------------------------------------------------------------------- 1 | html h1 { 2 | font-size: 26px; 3 | margin-left: 10px; 4 | } 5 | 6 | html h2 { 7 | font-size: 22px; 8 | margin-left: 10px; 9 | } 10 | 11 | html h3 { 12 | font-size: 14px; 13 | margin-left: 10px; 14 | } 15 | 16 | html h4 { 17 | font-size: 16px; 18 | } 19 | 20 | .progtrckr { 21 | text-align: center; 22 | padding-bottom: 16px; 23 | border-bottom: solid 0px; 24 | } 25 | 26 | .progtrckr li { 27 | margin-bottom: 10px; 28 | } 29 | 30 | .val-err-tooltip { 31 | background-color: red; 32 | padding: 3px 5px 3px 10px; 33 | font-size: 14px; 34 | color: #fff; 35 | } 36 | 37 | .step { 38 | background-color: transparent; 39 | min-height: 450px; 40 | padding: 10px; 41 | } 42 | 43 | html .row, html .form-horizontal .form-group { 44 | margin: 0; 45 | } 46 | 47 | .footer-buttons { 48 | margin-top: 10px; 49 | } 50 | 51 | html .step3 label, html .step4 label { 52 | /*font-size: 20px;*/ 53 | text-align: left; 54 | } 55 | 56 | html .form-horizontal .control-label { 57 | text-align: left; 58 | } 59 | 60 | .review .txt { 61 | font-size: 20px; 62 | text-align: left; 63 | margin: 0; 64 | padding: 0; 65 | } 66 | 67 | html body .saving { 68 | background-color: #5cb85c; 69 | width: 90%; 70 | padding: 5px; 71 | font-size: 16px; 72 | } 73 | 74 | code { 75 | position: relative; 76 | left: 12px; 77 | line-height: 25px; 78 | } 79 | 80 | .eg-jump-lnk { 81 | margin-top: 50px; 82 | font-style: italic; 83 | } 84 | 85 | .lib-version { 86 | font-size: 12px; 87 | background-color: rgba(255, 255, 0, 0.38); 88 | position: absolute; 89 | right: 10px; 90 | top: 10px; 91 | padding: 5px; 92 | } 93 | 94 | html .content { 95 | margin-left: 10px; 96 | } 97 | 98 | span.red { 99 | color: #d9534f; 100 | } 101 | 102 | span.green { 103 | color: #3c763d; 104 | } 105 | 106 | span.bold { 107 | font-weight: bold; 108 | } 109 | 110 | html .hoc-alert { 111 | margin-top: 20px; 112 | } 113 | 114 | html .form-block-holder { 115 | margin-top: 20px !important; 116 | } -------------------------------------------------------------------------------- /src/examples/Step3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component, PropTypes } from 'react'; 4 | import Promise from 'promise'; 5 | 6 | export default class Step3 extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | saving: false 12 | }; 13 | 14 | this.isValidated = this.isValidated.bind(this); 15 | } 16 | 17 | componentDidMount() {} 18 | 19 | componentWillUnmount() {} 20 | 21 | // This review screen had the 'Save' button, on clicking this is called 22 | isValidated() { 23 | 24 | this.setState({ 25 | saving: true 26 | }); 27 | 28 | return new Promise((resolve, reject) => { 29 | setTimeout(() => { 30 | this.setState({ 31 | saving: true 32 | }); 33 | 34 | this.props.updateStore({savedToCloud: true}); // Update store here (this is just an example, in reality you will do it via redux or flux) 35 | 36 | // call resolve() to indicate that server validation or other aync method was a success. 37 | // ... only then will it move to the next step. reject() will indicate a fail 38 | resolve(); 39 | // reject(); // or reject 40 | }, 5000); 41 | }); 42 | } 43 | 44 | jumpToStep(toStep) { 45 | // We can explicitly move to a step (we -1 as its a zero based index) 46 | this.props.jumpToStep(toStep-1); // The StepZilla library injects this jumpToStep utility into each component 47 | } 48 | 49 | showValue() { 50 | console.log("Email: "+this.props.getStore().email+", Password: "+this.props.getStore().password+", Date: "+this.props.getStore().day+ 51 | "/"+this.props.getStore().month+"/"+this.props.getStore().year+", Gender: "+this.props.getStore().gender); 52 | } 53 | 54 | render() { 55 | const savingCls = this.state.saving ? 'saving col-md-12 show' : 'saving col-md-12 hide'; 56 | 57 | return ( 58 |
59 |
60 | 61 |
62 |
63 |
64 | 65 |
66 |
67 |
68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stepzilla", 3 | "version": "4.3.4", 4 | "description": "A react multi-step, wizard component for managing data collection via forms and sub components", 5 | "main": "./dist/main.js", 6 | "scripts": { 7 | "build": "node ./node_modules/gulp/bin/gulp.js build", 8 | "lint": "node ./node_modules/gulp/bin/gulp.js lint", 9 | "test": "node ./node_modules/gulp/bin/gulp.js test", 10 | "test:watch": "node ./node_modules/gulp/bin/gulp.js test-watch", 11 | "test:coverage": "node ./node_modules/gulp/bin/gulp.js test-coverage", 12 | "prepublish": "npm run build", 13 | "example": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js --progress --colors", 14 | "build-example": "node ./node_modules/gulp/bin/gulp.js build-example" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/newbreedofgeek/react-stepzilla.git" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "multistep", 23 | "react", 24 | "wizard", 25 | "react" 26 | ], 27 | "author": "@newbreedofgeek", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/newbreedofgeek/react-stepzilla/issues" 31 | }, 32 | "homepage": "https://github.com/newbreedofgeek/react-stepzilla#readme", 33 | "devDependencies": { 34 | "babel-core": "^6.21.0", 35 | "babel-eslint": "^7.1.1", 36 | "babel-loader": "^6.2.4", 37 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 38 | "babel-preset-es2015": "^6.9.0", 39 | "babel-preset-react": "^6.5.0", 40 | "chai": "^3.5.0", 41 | "css-loader": "^0.23.1", 42 | "del": "^2.2.2", 43 | "enzyme": "^2.7.0", 44 | "eslint": "^3.17.1", 45 | "eslint-config-airbnb": "^13.0.0", 46 | "eslint-plugin-import": "^2.2.0", 47 | "eslint-plugin-jsx-a11y": "^4.0.0", 48 | "eslint-plugin-react": "^6.10.0", 49 | "gulp": "^3.9.1", 50 | "gulp-babel": "^6.1.2", 51 | "gulp-eslint": "^3.0.1", 52 | "gulp-istanbul": "^1.1.1", 53 | "gulp-load-plugins": "^1.4.0", 54 | "gulp-mocha": "^3.0.1", 55 | "gulp-plumber": "^1.1.0", 56 | "isparta": "^4.0.0", 57 | "joi": "^10.2.2", 58 | "joi-validation-strategy": "^0.3.3", 59 | "json-loader": "^0.5.4", 60 | "promise": "^7.1.1", 61 | "react": "^15.3.2", 62 | "react-addons-test-utils": "^15.3.2", 63 | "react-dom": "^15.3.2", 64 | "react-validation-mixin": "^5.4.0", 65 | "sinon": "^1.17.7", 66 | "sinon-chai": "^2.8.0", 67 | "style-loader": "^0.13.1", 68 | "webpack": "^1.13.1", 69 | "webpack-dev-server": "^1.14.1", 70 | "webpack-stream": "^3.2.0" 71 | }, 72 | "dependencies": { 73 | "babel-loader": "^6.2.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var del = require('del'); 2 | var gulp = require('gulp'); 3 | var loadPlugins = require('gulp-load-plugins'); 4 | var isparta = require('isparta'); 5 | var webpack = require('webpack'); 6 | var webpackConfig = require("./webpack.config.js"); 7 | 8 | // Load all of our Gulp plugins 9 | var $ = loadPlugins(); 10 | 11 | function _registerBabel() { 12 | require('babel-core/register'); 13 | } 14 | 15 | // Clean up 16 | gulp.task('clean', function(){ 17 | del.sync(['./dist','./dist.zip']); 18 | }); 19 | 20 | // Lint 21 | gulp.task('lint', function() { 22 | return gulp.src('./src/main.js') 23 | .pipe($.plumber()) 24 | .pipe($.eslint()) 25 | .pipe($.eslint.format()) 26 | .pipe($.eslint.failOnError()); 27 | }); 28 | 29 | // Test 30 | gulp.task('test', function() { 31 | _registerBabel(); 32 | 33 | return gulp.src(['./tests/**/*.spec.js'], {read: false}) 34 | .pipe($.mocha({ 35 | reporter: 'spec', 36 | globals: ["sinon", "chai", "expect"], 37 | require: ['./tests/test-helper.js'] 38 | })); 39 | }); 40 | 41 | // Live Test Watching 42 | gulp.task('test-watch', ['test'], function() { 43 | gulp.watch(['./tests/**/*.spec.js', './src/main.js'], ['test']); 44 | }); 45 | 46 | // Coverage 47 | gulp.task('test-coverage', function () { 48 | _registerBabel(); 49 | 50 | return gulp.src(['./src/main.js']) 51 | .pipe($.istanbul({ // Covering filecs 52 | instrumenter: isparta.Instrumenter, 53 | includeUntested: true })) 54 | .pipe($.istanbul.hookRequire()) // Force `require` to return covered files 55 | .on('finish', () => { 56 | gulp.src(['./tests/**/*.spec.js'], {read: false}) 57 | .pipe($.mocha({ 58 | reporter: 'spec', 59 | globals: ["sinon", "chai", "expect"], 60 | require: ['./tests/test-helper.js'] })) 61 | .pipe($.istanbul.writeReports()); 62 | }); 63 | }); 64 | 65 | // Build 66 | gulp.task('build', ['clean'], function () { 67 | return gulp.src('./src/main.js') 68 | .pipe($.babel()) //this will also handle react transformations 69 | .pipe(gulp.dest('./dist')); 70 | }); 71 | 72 | // Build the example into 'docs' so we can ghpages it 73 | gulp.task('build-example', ['build'], function () { 74 | del.sync('./docs'); 75 | 76 | webpack(webpackConfig, function(err, stats) { 77 | if(err) throw new Error("webpack", err); 78 | 79 | console.log('webpack build done...'); 80 | 81 | gulp.src('./src/examples/dist/src/examples/dist/bundle.js') 82 | .pipe(gulp.dest('./docs/dist')); 83 | 84 | gulp.src('./dist/main.js') 85 | .pipe(gulp.dest('./docs')); 86 | 87 | gulp.src('./src/examples/index.html') 88 | .pipe(gulp.dest('./docs/')); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React StepZilla - Demo Page 6 | 7 | 8 | 127 | 128 | 129 | 132 |
v4.3.4
133 |
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/examples/Step1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component, PropTypes } from 'react'; 4 | 5 | export default class Step1 extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | email: props.getStore().email, 11 | password: props.getStore().password 12 | }; 13 | 14 | this._validateOnDemand = true; // this flag enables onBlur validation as user fills forms 15 | 16 | this.validationCheck = this.validationCheck.bind(this); 17 | this.isValidated = this.isValidated.bind(this); 18 | } 19 | 20 | componentDidMount() {} 21 | 22 | componentWillUnmount() {} 23 | 24 | isValidated() { 25 | const userInput = this._grabUserInput(); // grab user entered vals 26 | const validateNewInput = this._validateData(userInput); // run the new input against the validator 27 | let isDataValid = false; 28 | 29 | // if full validation passes then save to store and pass as valid 30 | if (Object.keys(validateNewInput).every((k) => { return validateNewInput[k] === true })) { 31 | if (this.props.getStore().email != userInput.email || this.props.getStore().password != userInput.password) { // only update store of something changed 32 | this.props.updateStore({ 33 | ...userInput, 34 | savedToCloud: false // use this to notify step4 that some changes took place and prompt the user to save again 35 | }); // Update store here (this is just an example, in reality you will do it via redux or flux) 36 | } 37 | 38 | isDataValid = true; 39 | } 40 | else { 41 | // if anything fails then update the UI validation state but NOT the UI Data State 42 | this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput))); 43 | } 44 | 45 | return isDataValid; 46 | } 47 | 48 | validationCheck() { 49 | this.isValidated(); 50 | if (!this._validateOnDemand) 51 | return; 52 | 53 | const userInput = this._grabUserInput(); // grab user entered vals 54 | const validateNewInput = this._validateData(userInput); // run the new input against the validator 55 | 56 | this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput))); 57 | } 58 | 59 | _validateData(data) { 60 | return { 61 | emailVal: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(data.email), // required: regex w3c uses in html5 62 | passwordVal: (data.password != '' && data.password.length > 5), 63 | confirmpasswordVal: (data.confirmpassword == data.password) 64 | } 65 | } 66 | 67 | _validationErrors(val) { 68 | const errMsgs = { 69 | emailValMsg: val.emailVal ? '' : 'A valid email is required', 70 | passwordValMsg: val.passwordVal ? '' : 'A valid password is required', 71 | confirmpasswordValMsg: val.confirmpasswordVal ? '' : 'A valid confirmpassword is required' 72 | } 73 | return errMsgs; 74 | } 75 | 76 | _grabUserInput() { 77 | return { 78 | email: this.refs.email.value, 79 | password: this.refs.password.value, 80 | confirmpassword: this.refs.confirmpassword.value 81 | }; 82 | } 83 | 84 | render() { 85 | // explicit class assigning based on validation 86 | let notValidClasses = {}; 87 | 88 | if (typeof this.state.emailVal == 'undefined' || this.state.emailVal) { 89 | notValidClasses.emailCls = 'no-error col-md-8'; 90 | } 91 | else { 92 | notValidClasses.emailCls = 'has-error col-md-8'; 93 | notValidClasses.emailValGrpCls = 'val-err-tooltip'; 94 | } 95 | 96 | if (typeof this.state.passwordVal == 'undefined' || this.state.passwordVal) { 97 | notValidClasses.passwordCls = 'no-error col-md-8'; 98 | } 99 | else { 100 | notValidClasses.passwordCls = 'has-error col-md-8'; 101 | notValidClasses.passwordValGrpCls = 'val-err-tooltip'; 102 | } 103 | 104 | if (typeof this.state.confirmpasswordVal == 'undefined' || this.state.confirmpasswordVal) { 105 | notValidClasses.confirmpasswordCls = 'no-error col-md-8'; 106 | } 107 | else { 108 | notValidClasses.confirmpasswordCls = 'has-error col-md-8'; 109 | notValidClasses.confirmpasswordValGrpCls = 'val-err-tooltip'; 110 | } 111 | 112 | return ( 113 |
114 |
115 |
116 |
117 | 118 | 126 |
{this.state.emailValMsg}
127 |
128 |
129 |
130 |
131 | 132 | 139 |
{this.state.passwordValMsg}
140 |
141 |
142 |
143 |
144 | 145 | 151 |
{this.state.confirmpasswordValMsg}
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | ) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* forcing vertical scroll */ 2 | html { 3 | overflow-y: scroll; 4 | } 5 | /* Shared */ 6 | .button { 7 | border-radius: 100px; 8 | } 9 | 10 | /* Sections */ 11 | .section { 12 | padding: 2rem 0 2rem; 13 | text-align: center; 14 | } 15 | .section-heading, 16 | .section-description { 17 | margin-bottom: 3rem; 18 | } 19 | 20 | .required { 21 | color: #FF6000; 22 | } 23 | 24 | /* value-img */ 25 | .value-img { 26 | display: block; 27 | margin-left: auto; 28 | margin-right: auto; 29 | margin-top: 2.5rem; 30 | margin-bottom: 2.5rem; 31 | } 32 | 33 | /* docs */ 34 | .docs-section { 35 | border-top: 1px solid #eee; 36 | margin-top: 30px; 37 | margin-bottom: 0; 38 | } 39 | .docs-terms { 40 | font-size: 1.2rem; 41 | letter-spacing: .2rem; 42 | font-weight: 800; 43 | } 44 | 45 | .fieldset-section { 46 | width: 90%; 47 | margin-left: 10px; 48 | margin-top: 10px; 49 | margin:0; 50 | } 51 | 52 | #root { 53 | margin-top: 100px !important; 54 | width: 30% !important; 55 | border: 1px solid black !important; 56 | border-radius: 20px !important; 57 | } 58 | 59 | .footerline { 60 | margin-top: 88px; 61 | color: black; 62 | } 63 | 64 | button.u-date-width { 65 | font-size: 11px; 66 | } 67 | 68 | .row { 69 | margin-top: 20px !important; 70 | } 71 | 72 | .header { 73 | background-color: transparent !important; 74 | } 75 | 76 | .row_dashimg { 77 | margin-top: 50px; 78 | } 79 | 80 | .row_godash { 81 | margin-top: 110px; 82 | } 83 | 84 | /* legend --- ---- --- */ 85 | .legend-section { 86 | text-transform: uppercase; 87 | font-size: 1.4rem; 88 | font-weight: 800; 89 | border-style:none; 90 | border-bottom: 1px solid #E1E1E1; 91 | background-color: #fff; 92 | color: black; 93 | width:100%; 94 | margin-bottom:8px; 95 | } 96 | .legend-action { 97 | border-bottom: 4px solid #E1E1E1; 98 | background-color: #fff; 99 | position: relative; 100 | float: right; 101 | margin-left:5px; 102 | } 103 | .legend-action:hover { 104 | background-color: #f6f6f6; 105 | color: #33C3F0; 106 | } 107 | .legend-action.active { 108 | color: #33C3F0; 109 | } 110 | @media (min-width: 650px) { 111 | .legend-section { 112 | font-size: 1.6rem; 113 | letter-spacing: .2rem; 114 | margin-bottom:18px; 115 | } 116 | } 117 | 118 | /* navbar --- ---- */ 119 | .navbar { 120 | margin-left: auto; 121 | margin-right: auto; 122 | display: block; 123 | height: 3rem; 124 | border-bottom: 4px solid #EEE; 125 | } 126 | .navbar + .docs-section { 127 | border-top-width: 0; 128 | } 129 | .navbar-list { 130 | list-style: none; 131 | margin-bottom: 0; 132 | } 133 | .navbar-item { 134 | position: relative; 135 | float: left; 136 | text-transform: uppercase; 137 | font-size: 1.5rem; 138 | font-weight: 800; 139 | margin-right: .8rem; 140 | margin-left: 0; 141 | margin-bottom: 0; 142 | line-height: 4.2rem; 143 | border-style:none; 144 | } 145 | .navbar-item.right { 146 | float: right; 147 | margin-right: 0; 148 | margin-left: .8rem; 149 | } 150 | .navbar-link { 151 | text-decoration: none; 152 | color: #222; 153 | border-style:none; 154 | } 155 | .navbar-link.active { 156 | background-color: #f6f6f6; 157 | border: 0; 158 | border-bottom: 4px solid #33C3F0; 159 | color: #33C3F0; 160 | } 161 | .navbar-link:hover { 162 | background-color: #f6f6f6; 163 | border-bottom: 4px solid #33C3F0; 164 | color: #33C3F0; 165 | } 166 | @media (min-width: 550px) { 167 | .navbar { 168 | height: 4rem; 169 | width: 80%; 170 | } 171 | .navbar-item { 172 | font-size: 2rem; 173 | line-height: 5.6rem; 174 | margin-right: 2rem; 175 | } 176 | } 177 | 178 | /* Content link */ 179 | .content-link { 180 | background: #eee; 181 | text-transform: uppercase; 182 | font-size: 1.5rem; 183 | font-weight: 600; 184 | letter-spacing: .1rem; 185 | margin: 5px; 186 | text-decoration: none; 187 | color: #33C3F0; 188 | } 189 | .content-link:hover { 190 | color: #33C3F0; 191 | border-bottom: 2px solid #33C3F0; 192 | } 193 | @media (min-width: 550px) { 194 | .content-link { 195 | font-size: 1.8rem; 196 | } 197 | .content-link:hover { 198 | border-bottom: 3px solid #33C3F0; 199 | } 200 | } 201 | 202 | /* table */ 203 | table { 204 | border-style: none; 205 | border-top-width: 0; 206 | width: auto; 207 | } 208 | table tr { 209 | border-style: none; 210 | border-bottom-width: 0; 211 | } 212 | table th, table td { 213 | border-style: none; 214 | padding-right: 1rem; 215 | padding-top: 0.75rem; 216 | padding-bottom: 0.75rem; 217 | text-align: left; 218 | min-width: 50px; 219 | vertical-align: top; 220 | } 221 | table th.tool, table td.tool { 222 | padding: 0 1rem; 223 | } 224 | table th + th, table th + td, table td + th, table td + td { 225 | border-left-width: 1px; 226 | } 227 | table thead tr:last-child { 228 | border-bottom-width: 2px; 229 | } 230 | table thead th, table tr.index th { 231 | font-size: 1.13333em; 232 | line-height: 1.41176rem; 233 | font-family: Helvetica, Arial, sans-serif; 234 | font-weight: bold; 235 | line-height: 1.25; 236 | text-transform: uppercase; 237 | } 238 | table thead th { 239 | border-bottom: 1px solid; 240 | padding-bottom: .25rem; 241 | } 242 | table tr.index th { 243 | font-size: 1.33333em; 244 | line-height: 1.2rem; 245 | } 246 | table tbody:first-of-type tr.index th { 247 | padding-top: 1rem; 248 | } 249 | table tbody th { 250 | font-weight: normal; 251 | } 252 | @media (max-width: 47.9375rem) { 253 | /* @media (max-width: 550px) { */ 254 | table { 255 | border: 0; 256 | border-bottom-width: 0.0625rem; 257 | border-bottom-style: solid; 258 | padding-bottom: 1.4375rem; 259 | padding-bottom: 0; 260 | display: block; 261 | width: 100%; 262 | /* 263 | * make everything display block so it 264 | * aligns vertically 265 | */ 266 | /* Labeling 267 | * adding a data-title attribute to the cells 268 | * lets us add text before the content to provide 269 | * the missing context 270 | * 271 | * Markup: 272 | * Content Here 273 | * 274 | * Display: 275 | * Column Header: Content Here 276 | */ 277 | } 278 | table caption { 279 | display: block; 280 | } 281 | table thead { 282 | display: none; 283 | visibility: hidden; 284 | } 285 | table tbody, table tr, table th, table td { 286 | border: 0; 287 | display: block; 288 | padding: 0; 289 | text-align: left; 290 | white-space: normal; 291 | } 292 | table tr { 293 | margin-bottom: 1.5rem; 294 | } 295 | table th[data-title]:before, table td[data-title]:before { 296 | content: attr(data-title) ": "; 297 | font-weight: bold; 298 | } 299 | table th:not([data-title]) { 300 | display: none; 301 | font-weight: bold; 302 | } 303 | table td:empty { 304 | display: none; 305 | } 306 | } 307 | 308 | /* footer */ 309 | .app-footer { 310 | color: silver; 311 | position: relative; 312 | line-height: 0.1rem; 313 | margin-top: 6rem; 314 | } 315 | -------------------------------------------------------------------------------- /src/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /src/examples/Step2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component, PropTypes } from 'react'; 4 | 5 | export default class Step2 extends Component { 6 | // constructor(props) { 7 | // super(props); 8 | 9 | // this.state = { 10 | // emailEmergency: props.getStore().emailEmergency 11 | // }; 12 | 13 | // this.validatorTypes = { 14 | // emailEmergency: Joi.string().email().required() 15 | // }; 16 | 17 | // this.getValidatorData = this.getValidatorData.bind(this); 18 | // this.renderHelpText = this.renderHelpText.bind(this); 19 | // this.isValidated = this.isValidated.bind(this); 20 | // } 21 | 22 | // isValidated() { 23 | // return new Promise((resolve, reject) => { 24 | // this.props.validate((error) => { 25 | // if (error) { 26 | // reject(); // form contains errors 27 | // return; 28 | // } 29 | 30 | // if (this.props.getStore().emailEmergency != this.getValidatorData().emailEmergency) { // only update store of something changed 31 | // this.props.updateStore({ 32 | // ...this.getValidatorData(), 33 | // savedToCloud: false // use this to notify step4 that some changes took place and prompt the user to save again 34 | // }); // Update store here (this is just an example, in reality you will do it via redux or flux) 35 | // } 36 | 37 | // resolve(); // form is valid, fire action 38 | // }); 39 | // }); 40 | // } 41 | 42 | // getValidatorData() { 43 | // return { 44 | // emailEmergency: this.refs.emailEmergency.value, 45 | // } 46 | // }; 47 | 48 | // onChange(e) { 49 | // let newState = {}; 50 | // newState[e.target.name] = e.target.value; 51 | // this.setState(newState); 52 | // } 53 | 54 | // renderHelpText(message, id) { 55 | // return (
{message}
); 56 | // }; 57 | 58 | // render() { 59 | // // explicit class assigning based on validation 60 | // let notValidClasses = {}; 61 | // notValidClasses.emailEmergencyCls = this.props.isValid('emailEmergency') ? 62 | // 'no-error col-md-8' : 'has-error col-md-8'; 63 | constructor(props) { 64 | super(props); 65 | 66 | this.state = { 67 | day: props.getStore().day, 68 | month: props.getStore().month, 69 | year: props.getStore().year, 70 | gender: props.getStore().gender 71 | }; 72 | 73 | this.genderValue = ''; 74 | 75 | this._validateOnDemand = true; // this flag enables onBlur validation as user fills forms 76 | 77 | this.validationCheck = this.validationCheck.bind(this); 78 | this.isValidated = this.isValidated.bind(this); 79 | } 80 | 81 | componentDidMount() {} 82 | 83 | componentWillUnmount() {} 84 | 85 | isValidated() { 86 | const userInput = this._grabUserInput(); // grab user entered vals 87 | const validateNewInput = this._validateData(userInput); // run the new input against the validator 88 | 89 | let isDataValid = false; 90 | // if full validation passes then save to store and pass as valid 91 | if (Object.keys(validateNewInput).every((k) => { return validateNewInput[k] === true })) { 92 | if (this.props.getStore().day != userInput.day || this.props.getStore().month != userInput.month || this.props.getStore().year != userInput.year || this.props.getStore().gender != userInput.gender) { // only update store of something changed 93 | 94 | this.props.updateStore({ 95 | ...userInput, 96 | savedToCloud: false // use this to notify step4 that some changes took place and prompt the user to save again 97 | }); // Update store here (this is just an example, in reality you will do it via redux or flux) 98 | } 99 | isDataValid = true; 100 | } 101 | else { 102 | // if anything fails then update the UI validation state but NOT the UI Data State 103 | this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput))); 104 | } 105 | 106 | return isDataValid; 107 | } 108 | 109 | validationCheck() { 110 | this.isValidated(); 111 | if (!this._validateOnDemand) 112 | return; 113 | 114 | const userInput = this._grabUserInput(); // grab user entered vals 115 | const validateNewInput = this._validateData(userInput); // run the new input against the validator 116 | 117 | this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput))); 118 | } 119 | 120 | _validateData(data) { 121 | return { 122 | dayVal: (data.day > 0 && (data.month == 2 ? data.day < 29 : data.day < 31)), 123 | monthVal: (data.month > 0 && data.month < 13), 124 | yearVal: (data.year > 1900 && data.year < 1999), 125 | genderVal: (data.gender != '') 126 | } 127 | } 128 | 129 | _validationErrors(val) { 130 | const errMsgs = { 131 | dayValMsg: val.dayVal ? '' : 'A valid day', 132 | monthValMsg: val.monthVal ? '' : 'A valid month', 133 | yearValMsg: val.yearVal ? '' : 'A valid year', 134 | dateValMsg: val.dayVal && val.monthVal && val.yearVal ? '' : 'A valid date is required', 135 | genderValMsg: val.genderVal ? '' : 'A valid gender is required' 136 | } 137 | return errMsgs; 138 | } 139 | 140 | _grabUserInput() { 141 | return { 142 | day: this.refs.day.value, 143 | month: this.refs.month.value, 144 | year: this.refs.year.value, 145 | gender: this.genderValue 146 | }; 147 | } 148 | 149 | maleBtn() { 150 | this.genderValue = 'male'; 151 | this.props.getStore().gender = 'male'; 152 | this.validationCheck(); 153 | } 154 | 155 | femaleBtn() { 156 | 157 | this.genderValue = 'female'; 158 | this.props.getStore().gender = 'female'; 159 | this.validationCheck(); 160 | } 161 | 162 | unspecifedBtn() { 163 | this.genderValue = 'unspecifed'; 164 | this.props.getStore().gender = 'unspecifed' 165 | this.validationCheck(); 166 | } 167 | 168 | render() { 169 | // explicit class assigning based on validation 170 | let notValidClasses = {}; 171 | 172 | if (typeof this.state.dayVal == 'undefined' || this.state.dayVal) { 173 | notValidClasses.dayCls = 'no-error col-md-8'; 174 | } 175 | else { 176 | notValidClasses.dayCls = 'has-error col-md-8'; 177 | notValidClasses.dayValGrpCls = 'val-err-tooltip'; 178 | notValidClasses.dateValGrpCls = 'val-err-tooltip'; 179 | } 180 | 181 | if (typeof this.state.monthVal == 'undefined' || this.state.monthVal) { 182 | notValidClasses.monthCls = 'no-error col-md-8'; 183 | } 184 | else { 185 | notValidClasses.monthCls = 'has-error col-md-8'; 186 | notValidClasses.monthValGrpCls = 'val-err-tooltip'; 187 | notValidClasses.dateValGrpCls = 'val-err-tooltip'; 188 | } 189 | 190 | if (typeof this.state.yearVal == 'undefined' || this.state.yearVal) { 191 | notValidClasses.yearCls = 'no-error col-md-8'; 192 | } 193 | else { 194 | notValidClasses.yearCls = 'has-error col-md-8'; 195 | notValidClasses.yearValGrpCls = 'val-err-tooltip'; 196 | notValidClasses.dateValGrpCls = 'val-err-tooltip'; 197 | } 198 | 199 | let buttonClasses = {maleCls: "u-date-width", femaleCls: "u-date-width", unspecifiedCls: "u-date-width"}; 200 | 201 | if (this.genderValue == 'male') { 202 | buttonClasses.maleCls = 'u-date-width button-primary'; 203 | buttonClasses.femaleCls = 'u-date-width'; 204 | buttonClasses.unspecifiedCls = 'u-date-width'; 205 | } 206 | else if (this.genderValue == 'female') { 207 | buttonClasses.maleCls = 'u-date-width'; 208 | buttonClasses.femaleCls = 'u-date-width button-primary'; 209 | buttonClasses.unspecifiedCls = 'u-date-width'; 210 | } 211 | else if (this.genderValue == 'unspecifed') { 212 | buttonClasses.maleCls = 'u-date-width'; 213 | buttonClasses.femaleCls = 'u-date-width'; 214 | buttonClasses.unspecifiedCls = 'u-date-width button-primary'; 215 | } 216 | else if (this.genderValue == ''){ 217 | buttonClasses.genderValGrpCls = 'val-err-tooltip'; 218 | } 219 | 220 | return ( 221 |
222 |
223 |
224 | 225 | 233 | 240 | 247 |
248 |
249 |
250 |
251 |
{this.state.dateValMsg}
252 |
253 |
254 |
255 |
256 | 257 | 260 | 262 | 264 |
265 |
266 |
267 |
268 |
{this.state.genderValMsg}
269 |
270 |
271 |
272 |
273 | 274 | 282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 | ) 291 | } 292 | } 293 | 294 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Promise from 'promise'; 3 | 4 | export default class StepZilla extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | ...this.getPrevNextBtnState(this.props.startAtStep), 10 | compState: this.props.startAtStep, 11 | navState: this.getNavStates(this.props.startAtStep, this.props.steps.length), 12 | }; 13 | 14 | this.hidden = { 15 | display: 'none' 16 | }; 17 | 18 | this.applyValidationFlagsToSteps(); 19 | } 20 | 21 | // extend the "steps" array with flags to indicate if they have been validated 22 | applyValidationFlagsToSteps() { 23 | this.props.steps.map((i) => { 24 | if (this.props.dontValidate) { 25 | i.validated = true; 26 | } 27 | else { 28 | i.validated = (typeof i.component.type.prototype.isValidated == 'undefined') ? true : false; 29 | } 30 | 31 | return i; 32 | }); 33 | } 34 | 35 | // update the header nav states via classes so they can be styled via css 36 | getNavStates(indx, length) { 37 | let styles = []; 38 | 39 | for (let i=0; i 0 && currentStep !== this.props.steps.length - 1) { 58 | if (currentStep === this.props.steps.length - 2) { 59 | correctNextText = this.props.nextTextOnFinalActionStep; // we are in the one before final step 60 | } 61 | return { 62 | showPreviousBtn: true, 63 | showNextBtn: true, 64 | nextStepText: correctNextText 65 | }; 66 | } else if (currentStep === 0) { 67 | return { 68 | showPreviousBtn: false, 69 | showNextBtn: true, 70 | nextStepText: correctNextText 71 | }; 72 | } 73 | return { 74 | showPreviousBtn: this.props.prevBtnOnLastStep, 75 | showNextBtn: false, 76 | nextStepText: correctNextText 77 | }; 78 | } 79 | 80 | // which step are we in? 81 | checkNavState(currentStep) { 82 | this.setState(this.getPrevNextBtnState(currentStep)); 83 | } 84 | 85 | // set the nav state 86 | setNavState(next) { 87 | this.setState({navState: this.getNavStates(next, this.props.steps.length)}); 88 | 89 | if (next < this.props.steps.length) { 90 | this.setState({compState: next}); 91 | } 92 | 93 | this.checkNavState(next); 94 | } 95 | 96 | // handles keydown on enter being pressed in any Child component input area. in this case it goes to the next 97 | handleKeyDown(evt) { 98 | if (evt.which === 13) { 99 | if (!this.props.preventEnterSubmission) { 100 | this.next(); 101 | } 102 | else { 103 | evt.preventDefault(); 104 | } 105 | } 106 | } 107 | 108 | // this utility method lets Child components invoke a direct jump to another step 109 | jumpToStep(evt) { 110 | if (evt.target == undefined) { 111 | // a child step wants to invoke a jump between steps. in this case 'evt' is the numeric step number and not the JS event 112 | this.setNavState(evt); 113 | } 114 | else { // the main navigation step ui is invoking a jump between steps 115 | if (!this.props.stepsNavigation || evt.target.value == this.state.compState) { // if stepsNavigation is turned off or user clicked on existing step again (on step 2 and clicked on 2 again) then ignore 116 | evt.preventDefault(); 117 | evt.stopPropagation(); 118 | 119 | return; 120 | } 121 | 122 | evt.persist(); // evt is a react event so we need to persist it as we deal with aync promises which nullifies these events (https://facebook.github.io/react/docs/events.html#event-pooling) 123 | 124 | const movingBack = evt.target.value < this.state.compState; // are we trying to move back or front? 125 | let passThroughStepsNotValid = false; // if we are jumping forward, only allow that if inbetween steps are all validated. This flag informs the logic... 126 | let proceed = false; // flag on if we should move on 127 | 128 | this.abstractStepMoveAllowedToPromise(movingBack) 129 | .then((valid = true) => { // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync v 130 | proceed = valid; 131 | 132 | if (!movingBack) { 133 | this.updateStepValidationFlag(proceed); 134 | } 135 | 136 | if (proceed) { 137 | if (!movingBack) { 138 | // looks like we are moving forward, 'reduce' a new array of step>validated values we need to check and 'some' that to get a decision on if we should allow moving forward 139 | passThroughStepsNotValid = this.props.steps 140 | .reduce((a, c, i) => { 141 | if (i >= this.state.compState && i < evt.target.value) { 142 | a.push(c.validated); 143 | } 144 | return a; 145 | }, []) 146 | .some((c) => { 147 | return c === false 148 | }) 149 | } 150 | } 151 | }) 152 | .catch((e) => { 153 | // Promise based validation was a fail (i.e reject()) 154 | if (!movingBack) { 155 | this.updateStepValidationFlag(false); 156 | } 157 | }) 158 | .then(() => { 159 | // this is like finally(), executes if error no no error 160 | if (proceed && !passThroughStepsNotValid) { 161 | if (evt.target.value === (this.props.steps.length - 1) && 162 | this.state.compState === (this.props.steps.length - 1)) { 163 | this.setNavState(this.props.steps.length); 164 | } 165 | else { 166 | this.setNavState(evt.target.value); 167 | } 168 | } 169 | }) 170 | .catch(e => { 171 | if (e) { 172 | // see note below called "CatchRethrowing" 173 | // ... plus the finally then() above is what throws the JS Error so we need to catch that here specifically 174 | setTimeout(function() { throw e; }); 175 | } 176 | }); 177 | } 178 | } 179 | 180 | // move next via next button 181 | next() { 182 | this.abstractStepMoveAllowedToPromise() 183 | .then((proceed = true) => { 184 | // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync validation 185 | this.updateStepValidationFlag(proceed); 186 | if (proceed) { 187 | this.setNavState(this.state.compState + 1); 188 | } 189 | }) 190 | .catch((e) => { 191 | if (e) { 192 | 193 | setTimeout(function() { throw e; }); 194 | } 195 | 196 | // Promise based validation was a fail (i.e reject()) 197 | this.updateStepValidationFlag(false); 198 | }); 199 | } 200 | 201 | // move behind via previous button 202 | previous() { 203 | if (this.state.compState > 0) { 204 | this.setNavState(this.state.compState - 1); 205 | } 206 | } 207 | 208 | // update step's validation flag 209 | updateStepValidationFlag(val = true) { 210 | this.props.steps[this.state.compState].validated = val; // note: if a step component returns 'underfined' then treat as "true". 211 | } 212 | 213 | // are we allowed to move forward? via the next button or via jumpToStep? 214 | stepMoveAllowed(skipValidationExecution = false) { 215 | let proceed = false; 216 | 217 | if (this.props.dontValidate) { 218 | proceed = true; 219 | } 220 | else { 221 | if (skipValidationExecution) { 222 | // we are moving backwards in steps, in this case dont validate as it means the user is not commiting to "save" 223 | proceed = true; 224 | } 225 | else if (this.props.hocValidationAppliedTo.length > 0 && this.props.hocValidationAppliedTo.indexOf(this.state.compState) > -1) { 226 | // the user is using a higer order component (HOC) for validation (e.g react-validation-mixin), this wraps the StepZilla steps as a HOC, so use hocValidationAppliedTo to determine if this step needs the aync validation as per react-validation-mixin interface 227 | proceed = this.refs.activeComponent.refs.component.isValidated(); 228 | } 229 | else if (Object.keys(this.refs).length == 0 || typeof this.refs.activeComponent.isValidated == 'undefined') { 230 | // if its a form component, it should have implemeted a public isValidated class (also pure componenets wont even have refs - i.e. a empty object). If not then continue 231 | proceed = true; 232 | } 233 | else { 234 | // user is moving forward in steps, invoke validation as its available 235 | proceed = this.refs.activeComponent.isValidated(); 236 | } 237 | } 238 | 239 | return proceed; 240 | } 241 | 242 | // a validation method is each step can be sync or async (Promise based), this utility abstracts the wrapper stepMoveAllowed to be Promise driven regardless of validation return type 243 | abstractStepMoveAllowedToPromise(movingBack) { 244 | return Promise.resolve(this.stepMoveAllowed(movingBack)); 245 | } 246 | 247 | // get the classmame of steps 248 | getClassName(className, i){ 249 | let liClassName = className + "-" + this.state.navState.styles[i]; 250 | 251 | // if step ui based navigation is disabled, then dont highlight step 252 | if (!this.props.stepsNavigation) 253 | liClassName += " no-hl"; 254 | 255 | return liClassName; 256 | } 257 | 258 | // render the steps as stepsNavigation 259 | renderSteps() { 260 | return this.props.steps.map((s, i)=> ( 261 |
  • {this.jumpToStep(evt)}} key={i} value={i}> 262 | {i+1} 263 | {this.props.steps[i].name} 264 |
  • 265 | )); 266 | } 267 | 268 | // main render of stepzilla container 269 | render() { 270 | let compToRender; 271 | 272 | // clone the step component dynamically and tag it as activeComponent so we can validate it on next. also bind the jumpToStep piping method 273 | let cloneExtensions = { 274 | jumpToStep: (t) => { 275 | this.jumpToStep(t); 276 | } 277 | }; 278 | 279 | const componentPointer = this.props.steps[this.state.compState].component; 280 | 281 | // can only update refs if its a regular React component (not a pure component), so lets check that 282 | if (componentPointer instanceof Component || // unit test deteceted that instanceof Component can be in either of these locations so test both (not sure why this is the case) 283 | (componentPointer.type && componentPointer.type.prototype instanceof Component)) { 284 | cloneExtensions.ref = 'activeComponent'; 285 | } 286 | 287 | compToRender = React.cloneElement(componentPointer, cloneExtensions); 288 | 289 | return ( 290 |
    {this.handleKeyDown(evt)}}> 291 | { 292 | this.props.showSteps 293 | ?
      294 | {this.renderSteps()} 295 |
    296 | : 297 | } 298 | 299 | {compToRender} 300 | 301 |
    302 | 305 | 306 | 309 |
    310 |
    311 | ); 312 | } 313 | } 314 | 315 | StepZilla.defaultProps = { 316 | showSteps: true, 317 | showNavigation: true, 318 | stepsNavigation: true, 319 | prevBtnOnLastStep: true, 320 | dontValidate: false, 321 | preventEnterSubmission: false, 322 | startAtStep: 0, 323 | nextTextOnFinalActionStep: "Next", 324 | hocValidationAppliedTo: [] 325 | }; 326 | -------------------------------------------------------------------------------- /src/css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 800px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; 38 | text-align: center; } 39 | .column, 40 | .columns { 41 | width: 100%; 42 | float: left; 43 | box-sizing: border-box; } 44 | 45 | /* For devices larger than 400px */ 46 | @media (min-width: 400px) { 47 | .container { 48 | /*width: 85%;*/ 49 | padding: 0; } 50 | } 51 | 52 | /* For devices larger than 550px */ 53 | @media (min-width: 550px) { 54 | .container { 55 | /*width: 80%; */ 56 | } 57 | .column, 58 | .columns { 59 | margin-left: 4%; } 60 | .column:first-child, 61 | .columns:first-child { 62 | margin-left: 0; } 63 | 64 | .one.column, 65 | .one.columns { width: 4.66666666667%; } 66 | .two.columns { width: 13.3333333333%; } 67 | .three.columns { width: 22%; } 68 | .four.columns { width: 30.6666666667%; } 69 | .five.columns { width: 39.3333333333%; } 70 | .six { width: 100%; } 71 | .seven.columns { width: 56.6666666667%; } 72 | .eight.columns { width: 65.3333333333%; } 73 | .nine.columns { width: 74.0%; } 74 | .ten.columns { width: 82.6666666667%; } 75 | .eleven.columns { width: 91.3333333333%; } 76 | .twelve.columns { width: 100%; margin-left: 0; } 77 | 78 | .one-third.column { width: 30.6666666667%; } 79 | .two-thirds.column { width: 65.3333333333%; } 80 | 81 | .one-half.column { width: 48%; } 82 | 83 | /* Offsets */ 84 | .offset-by-one.column, 85 | .offset-by-one.columns { margin-left: 8.66666666667%; } 86 | .offset-by-two.column, 87 | .offset-by-two.columns { margin-left: 17.3333333333%; } 88 | .offset-by-three.column, 89 | .offset-by-three.columns { margin-left: 26%; } 90 | .offset-by-four.column, 91 | .offset-by-four.columns { margin-left: 34.6666666667%; } 92 | .offset-by-five.column, 93 | .offset-by-five.columns { margin-left: 43.3333333333%; } 94 | .offset-by-six.column, 95 | .offset-by-six.columns { margin-left: 52%; } 96 | .offset-by-seven.column, 97 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 98 | .offset-by-eight.column, 99 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 100 | .offset-by-nine.column, 101 | .offset-by-nine.columns { margin-left: 78.0%; } 102 | .offset-by-ten.column, 103 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 104 | .offset-by-eleven.column, 105 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 106 | 107 | .offset-by-one-third.column, 108 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 109 | .offset-by-two-thirds.column, 110 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 111 | 112 | .offset-by-one-half.column, 113 | .offset-by-one-half.columns { margin-left: 52%; } 114 | 115 | } 116 | 117 | 118 | /* Base Styles 119 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 120 | /* NOTE 121 | html is set to 62.5% so that all the REM measurements throughout Skeleton 122 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 123 | html { 124 | font-size: 62.5%; } 125 | body { 126 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 127 | line-height: 1.6; 128 | font-weight: 400; 129 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 130 | color: #222; } 131 | 132 | 133 | /* Typography 134 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 135 | h1, h2, h3, h4, h5, h6 { 136 | margin-top: 0; 137 | margin-bottom: 2rem; 138 | font-weight: 300; } 139 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 140 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 141 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 142 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 143 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 144 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 145 | 146 | /* Larger than phablet */ 147 | @media (min-width: 550px) { 148 | h1 { font-size: 5.0rem; } 149 | h2 { font-size: 4.2rem; } 150 | h3 { font-size: 3.6rem; } 151 | h4 { font-size: 3.0rem; } 152 | h5 { font-size: 2.4rem; } 153 | h6 { font-size: 1.5rem; } 154 | } 155 | 156 | p { 157 | margin-top: 0; } 158 | 159 | 160 | /* Links 161 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 162 | a { 163 | color: #1EAEDB; } 164 | a:hover { 165 | color: #0FA0CE; } 166 | 167 | 168 | /* Buttons 169 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 170 | .button, 171 | button, 172 | input[type="submit"], 173 | input[type="reset"], 174 | input[type="button"] { 175 | display: inline-block; 176 | height: 38px; 177 | padding: 0 30px; 178 | color: #555; 179 | text-align: center; 180 | font-size: 11px; 181 | font-weight: 600; 182 | line-height: 38px; 183 | letter-spacing: .1rem; 184 | text-transform: uppercase; 185 | text-decoration: none; 186 | white-space: nowrap; 187 | background-color: transparent; 188 | border: 1px solid #bbb; 189 | cursor: pointer; 190 | box-sizing: border-box; } 191 | .button:hover, 192 | button:hover, 193 | input[type="submit"]:hover, 194 | input[type="reset"]:hover, 195 | input[type="button"]:hover, 196 | .button:focus, 197 | button:focus, 198 | input[type="submit"]:focus, 199 | input[type="reset"]:focus, 200 | input[type="button"]:focus { 201 | color: #333; 202 | border-color: #888; 203 | outline: 0; } 204 | .button.button-primary, 205 | button.button-primary, 206 | input[type="submit"].button-primary, 207 | input[type="reset"].button-primary, 208 | input[type="button"].button-primary { 209 | color: #FFF; 210 | background-color: #33C3F0; 211 | border-color: #33C3F0; } 212 | .button.button-primary:hover, 213 | button.button-primary:hover, 214 | input[type="submit"].button-primary:hover, 215 | input[type="reset"].button-primary:hover, 216 | input[type="button"].button-primary:hover, 217 | .button.button-primary:focus, 218 | button.button-primary:focus, 219 | input[type="submit"].button-primary:focus, 220 | input[type="reset"].button-primary:focus, 221 | input[type="button"].button-primary:focus { 222 | color: #FFF; 223 | background-color: #1EAEDB; 224 | border-color: #1EAEDB; } 225 | 226 | 227 | /* Forms 228 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 229 | input[type="email"], 230 | input[type="number"], 231 | input[type="date"], 232 | input[type="search"], 233 | input[type="text"], 234 | input[type="tel"], 235 | input[type="url"], 236 | input[type="password"], 237 | textarea, 238 | select { 239 | height: 38px; 240 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 241 | background-color: #fff; 242 | border: 1px solid #D1D1D1; 243 | border-radius: 4px; 244 | box-shadow: none; 245 | box-sizing: border-box; } 246 | /* Removes awkward default styles on some inputs for iOS */ 247 | input[type="email"], 248 | input[type="number"], 249 | input[type="date"], 250 | input[type="search"], 251 | input[type="text"], 252 | input[type="tel"], 253 | input[type="url"], 254 | input[type="password"], 255 | textarea { 256 | -webkit-appearance: none; 257 | -moz-appearance: none; 258 | appearance: none; } 259 | textarea { 260 | min-height: 65px; 261 | padding-top: 6px; 262 | padding-bottom: 6px; } 263 | input[type="email"]:focus, 264 | input[type="number"]:focus, 265 | input[type="date"]:focus, 266 | input[type="search"]:focus, 267 | input[type="text"]:focus, 268 | input[type="tel"]:focus, 269 | input[type="url"]:focus, 270 | input[type="password"]:focus, 271 | textarea:focus, 272 | select:focus { 273 | border: 1px solid #33C3F0; 274 | outline: 0; } 275 | label, 276 | legend { 277 | display: block; 278 | margin-bottom: .5rem; 279 | font-weight: 600; } 280 | fieldset { 281 | padding: 0; 282 | border-width: 0; } 283 | input[type="checkbox"], 284 | input[type="radio"] { 285 | display: inline; } 286 | label > .label-body { 287 | display: inline-block; 288 | margin-left: .5rem; 289 | font-weight: normal; } 290 | 291 | 292 | /* Lists 293 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 294 | ul { 295 | list-style: circle inside; } 296 | ol { 297 | list-style: decimal inside; } 298 | ol, ul { 299 | padding-left: 0; 300 | margin-top: 0; } 301 | ul ul, 302 | ul ol, 303 | ol ol, 304 | ol ul { 305 | margin: 1.5rem 0 1.5rem 3rem; 306 | font-size: 90%; } 307 | li { 308 | margin-bottom: 1rem; } 309 | 310 | 311 | /* Code 312 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 313 | code { 314 | padding: .2rem .5rem; 315 | margin: 0 .2rem; 316 | font-size: 90%; 317 | white-space: nowrap; 318 | background: #F1F1F1; 319 | border: 1px solid #E1E1E1; 320 | border-radius: 4px; } 321 | pre > code { 322 | display: block; 323 | padding: 1rem 1.5rem; 324 | white-space: pre; } 325 | 326 | 327 | /* Tables 328 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 329 | th, 330 | td { 331 | padding: 12px 15px; 332 | text-align: left; 333 | border-bottom: 1px solid #E1E1E1; } 334 | th:first-child, 335 | td:first-child { 336 | padding-left: 0; } 337 | th:last-child, 338 | td:last-child { 339 | padding-right: 0; } 340 | 341 | 342 | /* Spacing 343 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 344 | button, 345 | .button { 346 | margin-bottom: 1rem; } 347 | input, 348 | textarea, 349 | select, 350 | fieldset { 351 | margin-bottom: 1.5rem; } 352 | pre, 353 | blockquote, 354 | dl, 355 | figure, 356 | table, 357 | p, 358 | ul, 359 | ol, 360 | form { 361 | margin-bottom: 2.5rem; } 362 | 363 | 364 | /* Utilities 365 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 366 | .u-full-border-width { 367 | width: 100%; 368 | box-sizing: border-box; 369 | border-top-color: transparent !important; 370 | border-left-color: transparent !important; 371 | border-right-color: transparent !important; } 372 | .u-full-width { 373 | width: 100%; 374 | box-sizing: border-box; } 375 | .u-max-full-width { 376 | max-width: 100%; 377 | box-sizing: border-box; } 378 | .u-pull-right { 379 | float: right; } 380 | .u-pull-left { 381 | float: left; } 382 | .u-date-width { 383 | width: 33%; 384 | box-sizing: border-box; } 385 | .u-left-width { 386 | text-align: left !important; 387 | width:100%; } 388 | .u-center-width { 389 | text-align: center !important; 390 | width: 100%} 391 | .multistep__btn--prev { 392 | float: left; 393 | border-color: transparent; 394 | margin-bottom: 5px; } 395 | .multistep__btn--prev:hover { 396 | color: #33C3F0; 397 | border-color: transparent; 398 | outline: 0; } 399 | .multistep__btn--next { 400 | float: right; 401 | border-color: transparent; 402 | margin-bottom: 5px; } 403 | .multistep__btn--next:hover { 404 | color: #33C3F0; 405 | border-color: transparent; 406 | outline: 0; } 407 | 408 | 409 | /* Misc 410 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 411 | hr { 412 | margin-top: 3rem; 413 | margin-bottom: 3.5rem; 414 | border-width: 0; 415 | border-top: 1px solid #E1E1E1; } 416 | 417 | 418 | /* Clearing 419 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 420 | 421 | /* Self Clearing Goodness */ 422 | .container:after, 423 | .row:after, 424 | .u-cf { 425 | content: ""; 426 | display: table; 427 | clear: both; } 428 | 429 | 430 | /* Media Queries 431 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 432 | /* 433 | Note: The best way to structure the use of media queries is to create the queries 434 | near the relevant code. For example, if you wanted to change the styles for buttons 435 | on small devices, paste the mobile query code up in the buttons section and style it 436 | there. 437 | */ 438 | 439 | 440 | /* Larger than mobile */ 441 | @media (min-width: 400px) {} 442 | 443 | /* Larger than phablet (also point when grid becomes active) */ 444 | @media (min-width: 550px) {} 445 | 446 | /* Larger than tablet */ 447 | @media (min-width: 750px) {} 448 | 449 | /* Larger than desktop */ 450 | @media (min-width: 1000px) {} 451 | 452 | /* Larger than Desktop HD */ 453 | @media (min-width: 1200px) {} 454 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _promise = require('promise'); 16 | 17 | var _promise2 = _interopRequireDefault(_promise); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 22 | 23 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 24 | 25 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 26 | 27 | var StepZilla = function (_Component) { 28 | _inherits(StepZilla, _Component); 29 | 30 | function StepZilla(props) { 31 | _classCallCheck(this, StepZilla); 32 | 33 | var _this = _possibleConstructorReturn(this, (StepZilla.__proto__ || Object.getPrototypeOf(StepZilla)).call(this, props)); 34 | 35 | _this.state = _extends({}, _this.getPrevNextBtnState(_this.props.startAtStep), { 36 | compState: _this.props.startAtStep, 37 | navState: _this.getNavStates(_this.props.startAtStep, _this.props.steps.length) 38 | }); 39 | 40 | _this.hidden = { 41 | display: 'none' 42 | }; 43 | 44 | _this.applyValidationFlagsToSteps(); 45 | return _this; 46 | } 47 | 48 | // extend the "steps" array with flags to indicate if they have been validated 49 | 50 | 51 | _createClass(StepZilla, [{ 52 | key: 'applyValidationFlagsToSteps', 53 | value: function applyValidationFlagsToSteps() { 54 | var _this2 = this; 55 | 56 | this.props.steps.map(function (i) { 57 | if (_this2.props.dontValidate) { 58 | i.validated = true; 59 | } else { 60 | i.validated = typeof i.component.type.prototype.isValidated == 'undefined' ? true : false; 61 | } 62 | 63 | return i; 64 | }); 65 | } 66 | 67 | // update the header nav states via classes so they can be styled via css 68 | 69 | }, { 70 | key: 'getNavStates', 71 | value: function getNavStates(indx, length) { 72 | var styles = []; 73 | 74 | for (var i = 0; i < length; i++) { 75 | if (i < indx) { 76 | styles.push('done'); 77 | } else if (i === indx) { 78 | styles.push('doing'); 79 | } else { 80 | styles.push('todo'); 81 | } 82 | } 83 | 84 | return { current: indx, styles: styles }; 85 | } 86 | }, { 87 | key: 'getPrevNextBtnState', 88 | value: function getPrevNextBtnState(currentStep) { 89 | var correctNextText = 'Next'; 90 | 91 | if (currentStep > 0 && currentStep !== this.props.steps.length - 1) { 92 | if (currentStep === this.props.steps.length - 2) { 93 | correctNextText = this.props.nextTextOnFinalActionStep; // we are in the one before final step 94 | } 95 | return { 96 | showPreviousBtn: true, 97 | showNextBtn: true, 98 | nextStepText: correctNextText 99 | }; 100 | } else if (currentStep === 0) { 101 | return { 102 | showPreviousBtn: false, 103 | showNextBtn: true, 104 | nextStepText: correctNextText 105 | }; 106 | } 107 | return { 108 | showPreviousBtn: this.props.prevBtnOnLastStep, 109 | showNextBtn: false, 110 | nextStepText: correctNextText 111 | }; 112 | } 113 | 114 | // which step are we in? 115 | 116 | }, { 117 | key: 'checkNavState', 118 | value: function checkNavState(currentStep) { 119 | this.setState(this.getPrevNextBtnState(currentStep)); 120 | } 121 | 122 | // set the nav state 123 | 124 | }, { 125 | key: 'setNavState', 126 | value: function setNavState(next) { 127 | this.setState({ navState: this.getNavStates(next, this.props.steps.length) }); 128 | 129 | if (next < this.props.steps.length) { 130 | this.setState({ compState: next }); 131 | } 132 | 133 | this.checkNavState(next); 134 | } 135 | 136 | // handles keydown on enter being pressed in any Child component input area. in this case it goes to the next 137 | 138 | }, { 139 | key: 'handleKeyDown', 140 | value: function handleKeyDown(evt) { 141 | if (evt.which === 13) { 142 | if (!this.props.preventEnterSubmission) { 143 | this.next(); 144 | } else { 145 | evt.preventDefault(); 146 | } 147 | } 148 | } 149 | 150 | // this utility method lets Child components invoke a direct jump to another step 151 | 152 | }, { 153 | key: 'jumpToStep', 154 | value: function jumpToStep(evt) { 155 | var _this3 = this; 156 | 157 | if (evt.target == undefined) { 158 | // a child step wants to invoke a jump between steps. in this case 'evt' is the numeric step number and not the JS event 159 | this.setNavState(evt); 160 | } else { 161 | // the main navigation step ui is invoking a jump between steps 162 | if (!this.props.stepsNavigation || evt.target.value == this.state.compState) { 163 | // if stepsNavigation is turned off or user clicked on existing step again (on step 2 and clicked on 2 again) then ignore 164 | evt.preventDefault(); 165 | evt.stopPropagation(); 166 | 167 | return; 168 | } 169 | 170 | evt.persist(); // evt is a react event so we need to persist it as we deal with aync promises which nullifies these events (https://facebook.github.io/react/docs/events.html#event-pooling) 171 | 172 | var movingBack = evt.target.value < this.state.compState; // are we trying to move back or front? 173 | var passThroughStepsNotValid = false; // if we are jumping forward, only allow that if inbetween steps are all validated. This flag informs the logic... 174 | var proceed = false; // flag on if we should move on 175 | 176 | this.abstractStepMoveAllowedToPromise(movingBack).then(function () { 177 | var valid = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 178 | // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync v 179 | proceed = valid; 180 | 181 | if (!movingBack) { 182 | _this3.updateStepValidationFlag(proceed); 183 | } 184 | 185 | if (proceed) { 186 | if (!movingBack) { 187 | // looks like we are moving forward, 'reduce' a new array of step>validated values we need to check and 'some' that to get a decision on if we should allow moving forward 188 | passThroughStepsNotValid = _this3.props.steps.reduce(function (a, c, i) { 189 | if (i >= _this3.state.compState && i < evt.target.value) { 190 | a.push(c.validated); 191 | } 192 | return a; 193 | }, []).some(function (c) { 194 | return c === false; 195 | }); 196 | } 197 | } 198 | }).catch(function (e) { 199 | // Promise based validation was a fail (i.e reject()) 200 | if (!movingBack) { 201 | _this3.updateStepValidationFlag(false); 202 | } 203 | }).then(function () { 204 | // this is like finally(), executes if error no no error 205 | if (proceed && !passThroughStepsNotValid) { 206 | if (evt.target.value === _this3.props.steps.length - 1 && _this3.state.compState === _this3.props.steps.length - 1) { 207 | _this3.setNavState(_this3.props.steps.length); 208 | } else { 209 | _this3.setNavState(evt.target.value); 210 | } 211 | } 212 | }).catch(function (e) { 213 | if (e) { 214 | // see note below called "CatchRethrowing" 215 | // ... plus the finally then() above is what throws the JS Error so we need to catch that here specifically 216 | setTimeout(function () { 217 | throw e; 218 | }); 219 | } 220 | }); 221 | } 222 | } 223 | 224 | // move next via next button 225 | 226 | }, { 227 | key: 'next', 228 | value: function next() { 229 | var _this4 = this; 230 | 231 | this.abstractStepMoveAllowedToPromise().then(function () { 232 | var proceed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 233 | 234 | // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync validation 235 | _this4.updateStepValidationFlag(proceed); 236 | if (proceed) { 237 | _this4.setNavState(_this4.state.compState + 1); 238 | } 239 | }).catch(function (e) { 240 | if (e) { 241 | 242 | setTimeout(function () { 243 | throw e; 244 | }); 245 | } 246 | 247 | // Promise based validation was a fail (i.e reject()) 248 | _this4.updateStepValidationFlag(false); 249 | }); 250 | } 251 | 252 | // move behind via previous button 253 | 254 | }, { 255 | key: 'previous', 256 | value: function previous() { 257 | if (this.state.compState > 0) { 258 | this.setNavState(this.state.compState - 1); 259 | } 260 | } 261 | 262 | // update step's validation flag 263 | 264 | }, { 265 | key: 'updateStepValidationFlag', 266 | value: function updateStepValidationFlag() { 267 | var val = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 268 | 269 | this.props.steps[this.state.compState].validated = val; // note: if a step component returns 'underfined' then treat as "true". 270 | } 271 | 272 | // are we allowed to move forward? via the next button or via jumpToStep? 273 | 274 | }, { 275 | key: 'stepMoveAllowed', 276 | value: function stepMoveAllowed() { 277 | var skipValidationExecution = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 278 | 279 | var proceed = false; 280 | 281 | if (this.props.dontValidate) { 282 | proceed = true; 283 | } else { 284 | if (skipValidationExecution) { 285 | // we are moving backwards in steps, in this case dont validate as it means the user is not commiting to "save" 286 | proceed = true; 287 | } else if (this.props.hocValidationAppliedTo.length > 0 && this.props.hocValidationAppliedTo.indexOf(this.state.compState) > -1) { 288 | // the user is using a higer order component (HOC) for validation (e.g react-validation-mixin), this wraps the StepZilla steps as a HOC, so use hocValidationAppliedTo to determine if this step needs the aync validation as per react-validation-mixin interface 289 | proceed = this.refs.activeComponent.refs.component.isValidated(); 290 | } else if (Object.keys(this.refs).length == 0 || typeof this.refs.activeComponent.isValidated == 'undefined') { 291 | // if its a form component, it should have implemeted a public isValidated class (also pure componenets wont even have refs - i.e. a empty object). If not then continue 292 | proceed = true; 293 | } else { 294 | // user is moving forward in steps, invoke validation as its available 295 | proceed = this.refs.activeComponent.isValidated(); 296 | } 297 | } 298 | 299 | return proceed; 300 | } 301 | 302 | // a validation method is each step can be sync or async (Promise based), this utility abstracts the wrapper stepMoveAllowed to be Promise driven regardless of validation return type 303 | 304 | }, { 305 | key: 'abstractStepMoveAllowedToPromise', 306 | value: function abstractStepMoveAllowedToPromise(movingBack) { 307 | return _promise2.default.resolve(this.stepMoveAllowed(movingBack)); 308 | } 309 | 310 | // get the classmame of steps 311 | 312 | }, { 313 | key: 'getClassName', 314 | value: function getClassName(className, i) { 315 | var liClassName = className + "-" + this.state.navState.styles[i]; 316 | 317 | // if step ui based navigation is disabled, then dont highlight step 318 | if (!this.props.stepsNavigation) liClassName += " no-hl"; 319 | 320 | return liClassName; 321 | } 322 | 323 | // render the steps as stepsNavigation 324 | 325 | }, { 326 | key: 'renderSteps', 327 | value: function renderSteps() { 328 | var _this5 = this; 329 | 330 | return this.props.steps.map(function (s, i) { 331 | return _react2.default.createElement( 332 | 'li', 333 | { className: _this5.getClassName("progtrckr", i), onClick: function onClick(evt) { 334 | _this5.jumpToStep(evt); 335 | }, key: i, value: i }, 336 | _react2.default.createElement( 337 | 'em', 338 | null, 339 | i + 1 340 | ), 341 | _react2.default.createElement( 342 | 'span', 343 | null, 344 | _this5.props.steps[i].name 345 | ) 346 | ); 347 | }); 348 | } 349 | 350 | // main render of stepzilla container 351 | 352 | }, { 353 | key: 'render', 354 | value: function render() { 355 | var _this6 = this; 356 | 357 | var compToRender = void 0; 358 | 359 | // clone the step component dynamically and tag it as activeComponent so we can validate it on next. also bind the jumpToStep piping method 360 | var cloneExtensions = { 361 | jumpToStep: function jumpToStep(t) { 362 | _this6.jumpToStep(t); 363 | } 364 | }; 365 | 366 | var componentPointer = this.props.steps[this.state.compState].component; 367 | 368 | // can only update refs if its a regular React component (not a pure component), so lets check that 369 | if (componentPointer instanceof _react.Component || // unit test deteceted that instanceof Component can be in either of these locations so test both (not sure why this is the case) 370 | componentPointer.type && componentPointer.type.prototype instanceof _react.Component) { 371 | cloneExtensions.ref = 'activeComponent'; 372 | } 373 | 374 | compToRender = _react2.default.cloneElement(componentPointer, cloneExtensions); 375 | 376 | return _react2.default.createElement( 377 | 'div', 378 | { className: 'multi-step', onKeyDown: function onKeyDown(evt) { 379 | _this6.handleKeyDown(evt); 380 | } }, 381 | this.props.showSteps ? _react2.default.createElement( 382 | 'ol', 383 | { className: 'progtrckr' }, 384 | this.renderSteps() 385 | ) : _react2.default.createElement('span', null), 386 | compToRender, 387 | _react2.default.createElement( 388 | 'div', 389 | { style: this.props.showNavigation ? {} : this.hidden, className: 'footer-buttons' }, 390 | _react2.default.createElement( 391 | 'button', 392 | { style: this.state.showPreviousBtn ? {} : this.hidden, 393 | className: 'multistep__btn--prev', 394 | onClick: function onClick() { 395 | _this6.previous(); 396 | } }, 397 | 'Back' 398 | ), 399 | _react2.default.createElement( 400 | 'button', 401 | { style: this.state.showNextBtn ? {} : this.hidden, 402 | className: 'multistep__btn--next', 403 | onClick: function onClick() { 404 | _this6.next(); 405 | } }, 406 | 'Next->' 407 | ) 408 | ) 409 | ); 410 | } 411 | }]); 412 | 413 | return StepZilla; 414 | }(_react.Component); 415 | 416 | exports.default = StepZilla; 417 | 418 | 419 | StepZilla.defaultProps = { 420 | showSteps: true, 421 | showNavigation: true, 422 | stepsNavigation: true, 423 | prevBtnOnLastStep: true, 424 | dontValidate: false, 425 | preventEnterSubmission: false, 426 | startAtStep: 0, 427 | nextTextOnFinalActionStep: "Next", 428 | hocValidationAppliedTo: [] 429 | }; -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 10 | 11 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 12 | 13 | var _react = require('react'); 14 | 15 | var _react2 = _interopRequireDefault(_react); 16 | 17 | var _promise = require('promise'); 18 | 19 | var _promise2 = _interopRequireDefault(_promise); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var StepZilla = function (_Component) { 30 | _inherits(StepZilla, _Component); 31 | 32 | function StepZilla(props) { 33 | _classCallCheck(this, StepZilla); 34 | 35 | var _this = _possibleConstructorReturn(this, (StepZilla.__proto__ || Object.getPrototypeOf(StepZilla)).call(this, props)); 36 | 37 | _this.state = _extends({}, _this.getPrevNextBtnState(_this.props.startAtStep), { 38 | compState: _this.props.startAtStep, 39 | navState: _this.getNavStates(_this.props.startAtStep, _this.props.steps.length) 40 | }); 41 | 42 | _this.hidden = { 43 | display: 'none' 44 | }; 45 | 46 | _this.applyValidationFlagsToSteps(); 47 | return _this; 48 | } 49 | 50 | // extend the "steps" array with flags to indicate if they have been validated 51 | 52 | 53 | _createClass(StepZilla, [{ 54 | key: 'applyValidationFlagsToSteps', 55 | value: function applyValidationFlagsToSteps() { 56 | var _this2 = this; 57 | 58 | this.props.steps.map(function (i) { 59 | if (_this2.props.dontValidate) { 60 | i.validated = true; 61 | } else { 62 | i.validated = typeof i.component.type.prototype.isValidated == 'undefined' ? true : false; 63 | } 64 | 65 | return i; 66 | }); 67 | } 68 | 69 | // update the header nav states via classes so they can be styled via css 70 | 71 | }, { 72 | key: 'getNavStates', 73 | value: function getNavStates(indx, length) { 74 | var styles = []; 75 | 76 | for (var i = 0; i < length; i++) { 77 | if (i < indx) { 78 | styles.push('done'); 79 | } else if (i === indx) { 80 | styles.push('doing'); 81 | } else { 82 | styles.push('todo'); 83 | } 84 | } 85 | 86 | return { current: indx, styles: styles }; 87 | } 88 | }, { 89 | key: 'getPrevNextBtnState', 90 | value: function getPrevNextBtnState(currentStep) { 91 | var correctNextText = 'Next'; 92 | 93 | if (currentStep > 0 && currentStep !== this.props.steps.length - 1) { 94 | if (currentStep === this.props.steps.length - 2) { 95 | correctNextText = this.props.nextTextOnFinalActionStep; // we are in the one before final step 96 | } 97 | return { 98 | showPreviousBtn: true, 99 | showNextBtn: true, 100 | nextStepText: correctNextText 101 | }; 102 | } else if (currentStep === 0) { 103 | return { 104 | showPreviousBtn: false, 105 | showNextBtn: true, 106 | nextStepText: correctNextText 107 | }; 108 | } 109 | return { 110 | showPreviousBtn: this.props.prevBtnOnLastStep, 111 | showNextBtn: false, 112 | nextStepText: correctNextText 113 | }; 114 | } 115 | 116 | // which step are we in? 117 | 118 | }, { 119 | key: 'checkNavState', 120 | value: function checkNavState(currentStep) { 121 | this.setState(this.getPrevNextBtnState(currentStep)); 122 | } 123 | 124 | // set the nav state 125 | 126 | }, { 127 | key: 'setNavState', 128 | value: function setNavState(next) { 129 | this.setState({ navState: this.getNavStates(next, this.props.steps.length) }); 130 | 131 | if (next < this.props.steps.length) { 132 | this.setState({ compState: next }); 133 | } 134 | 135 | this.checkNavState(next); 136 | } 137 | 138 | // handles keydown on enter being pressed in any Child component input area. in this case it goes to the next 139 | 140 | }, { 141 | key: 'handleKeyDown', 142 | value: function handleKeyDown(evt) { 143 | if (evt.which === 13) { 144 | if (!this.props.preventEnterSubmission) { 145 | this.next(); 146 | } else { 147 | evt.preventDefault(); 148 | } 149 | } 150 | } 151 | 152 | // this utility method lets Child components invoke a direct jump to another step 153 | 154 | }, { 155 | key: 'jumpToStep', 156 | value: function jumpToStep(evt) { 157 | var _this3 = this; 158 | 159 | if (evt.target == undefined) { 160 | // a child step wants to invoke a jump between steps. in this case 'evt' is the numeric step number and not the JS event 161 | this.setNavState(evt); 162 | } else { 163 | var _ret = function () { 164 | // the main navigation step ui is invoking a jump between steps 165 | if (!_this3.props.stepsNavigation || evt.target.value == _this3.state.compState) { 166 | // if stepsNavigation is turned off or user clicked on existing step again (on step 2 and clicked on 2 again) then ignore 167 | evt.preventDefault(); 168 | evt.stopPropagation(); 169 | 170 | return { 171 | v: void 0 172 | }; 173 | } 174 | 175 | evt.persist(); // evt is a react event so we need to persist it as we deal with aync promises which nullifies these events (https://facebook.github.io/react/docs/events.html#event-pooling) 176 | 177 | var movingBack = evt.target.value < _this3.state.compState; // are we trying to move back or front? 178 | var passThroughStepsNotValid = false; // if we are jumping forward, only allow that if inbetween steps are all validated. This flag informs the logic... 179 | var proceed = false; // flag on if we should move on 180 | 181 | _this3.abstractStepMoveAllowedToPromise(movingBack).then(function () { 182 | var valid = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 183 | // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync v 184 | proceed = valid; 185 | 186 | if (!movingBack) { 187 | _this3.updateStepValidationFlag(proceed); 188 | } 189 | 190 | if (proceed) { 191 | if (!movingBack) { 192 | // looks like we are moving forward, 'reduce' a new array of step>validated values we need to check and 'some' that to get a decision on if we should allow moving forward 193 | passThroughStepsNotValid = _this3.props.steps.reduce(function (a, c, i) { 194 | if (i >= _this3.state.compState && i < evt.target.value) { 195 | a.push(c.validated); 196 | } 197 | return a; 198 | }, []).some(function (c) { 199 | return c === false; 200 | }); 201 | } 202 | } 203 | }).catch(function (e) { 204 | // Promise based validation was a fail (i.e reject()) 205 | if (!movingBack) { 206 | _this3.updateStepValidationFlag(false); 207 | } 208 | }).then(function () { 209 | // this is like finally(), executes if error no no error 210 | if (proceed && !passThroughStepsNotValid) { 211 | if (evt.target.value === _this3.props.steps.length - 1 && _this3.state.compState === _this3.props.steps.length - 1) { 212 | _this3.setNavState(_this3.props.steps.length); 213 | } else { 214 | _this3.setNavState(evt.target.value); 215 | } 216 | } 217 | }).catch(function (e) { 218 | if (e) { 219 | // see note below called "CatchRethrowing" 220 | // ... plus the finally then() above is what throws the JS Error so we need to catch that here specifically 221 | setTimeout(function () { 222 | throw e; 223 | }); 224 | } 225 | }); 226 | }(); 227 | 228 | if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; 229 | } 230 | } 231 | 232 | // move next via next button 233 | 234 | }, { 235 | key: 'next', 236 | value: function next() { 237 | var _this4 = this; 238 | 239 | this.abstractStepMoveAllowedToPromise().then(function () { 240 | var proceed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 241 | 242 | // validation was a success (promise or sync validation). In it was a Promise's resolve() then proceed will be undefined, so make it true. Or else 'proceed' will carry the true/false value from sync validation 243 | _this4.updateStepValidationFlag(proceed); 244 | 245 | if (proceed) { 246 | _this4.setNavState(_this4.state.compState + 1); 247 | } 248 | }).catch(function (e) { 249 | if (e) { 250 | // CatchRethrowing: as we wrap StepMoveAllowed() to resolve as a Promise, the then() is invoked and the next React Component is loaded. 251 | // ... during the render, if there are JS errors thrown (e.g. ReferenceError) it gets swallowed by the Promise library and comes in here (catch) 252 | // ... so we need to rethrow it outside the execution stack so it behaves like a notmal JS error (i.e. halts and prints to console) 253 | // 254 | setTimeout(function () { 255 | throw e; 256 | }); 257 | } 258 | 259 | // Promise based validation was a fail (i.e reject()) 260 | _this4.updateStepValidationFlag(false); 261 | }); 262 | } 263 | 264 | // move behind via previous button 265 | 266 | }, { 267 | key: 'previous', 268 | value: function previous() { 269 | if (this.state.compState > 0) { 270 | this.setNavState(this.state.compState - 1); 271 | } 272 | } 273 | 274 | // update step's validation flag 275 | 276 | }, { 277 | key: 'updateStepValidationFlag', 278 | value: function updateStepValidationFlag() { 279 | var val = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; 280 | 281 | this.props.steps[this.state.compState].validated = val; // note: if a step component returns 'underfined' then treat as "true". 282 | } 283 | 284 | // are we allowed to move forward? via the next button or via jumpToStep? 285 | 286 | }, { 287 | key: 'stepMoveAllowed', 288 | value: function stepMoveAllowed() { 289 | var skipValidationExecution = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 290 | 291 | var proceed = false; 292 | 293 | if (this.props.dontValidate) { 294 | proceed = true; 295 | } else { 296 | if (skipValidationExecution) { 297 | // we are moving backwards in steps, in this case dont validate as it means the user is not commiting to "save" 298 | proceed = true; 299 | } else if (this.props.hocValidationAppliedTo.length > 0 && this.props.hocValidationAppliedTo.indexOf(this.state.compState) > -1) { 300 | // the user is using a higer order component (HOC) for validation (e.g react-validation-mixin), this wraps the StepZilla steps as a HOC, so use hocValidationAppliedTo to determine if this step needs the aync validation as per react-validation-mixin interface 301 | proceed = this.refs.activeComponent.refs.component.isValidated(); 302 | } else if (Object.keys(this.refs).length == 0 || typeof this.refs.activeComponent.isValidated == 'undefined') { 303 | // if its a form component, it should have implemeted a public isValidated class (also pure componenets wont even have refs - i.e. a empty object). If not then continue 304 | proceed = true; 305 | } else { 306 | // user is moving forward in steps, invoke validation as its available 307 | proceed = this.refs.activeComponent.isValidated(); 308 | } 309 | } 310 | 311 | return proceed; 312 | } 313 | 314 | // a validation method is each step can be sync or async (Promise based), this utility abstracts the wrapper stepMoveAllowed to be Promise driven regardless of validation return type 315 | 316 | }, { 317 | key: 'abstractStepMoveAllowedToPromise', 318 | value: function abstractStepMoveAllowedToPromise(movingBack) { 319 | return _promise2.default.resolve(this.stepMoveAllowed(movingBack)); 320 | } 321 | 322 | // get the classmame of steps 323 | 324 | }, { 325 | key: 'getClassName', 326 | value: function getClassName(className, i) { 327 | var liClassName = className + "-" + this.state.navState.styles[i]; 328 | 329 | // if step ui based navigation is disabled, then dont highlight step 330 | if (!this.props.stepsNavigation) liClassName += " no-hl"; 331 | 332 | return liClassName; 333 | } 334 | 335 | // render the steps as stepsNavigation 336 | 337 | }, { 338 | key: 'renderSteps', 339 | value: function renderSteps() { 340 | var _this5 = this; 341 | 342 | return this.props.steps.map(function (s, i) { 343 | return _react2.default.createElement( 344 | 'li', 345 | { className: _this5.getClassName("progtrckr", i), onClick: function onClick(evt) { 346 | _this5.jumpToStep(evt); 347 | }, key: i, value: i }, 348 | _react2.default.createElement( 349 | 'em', 350 | null, 351 | i + 1 352 | ), 353 | _react2.default.createElement( 354 | 'span', 355 | null, 356 | _this5.props.steps[i].name 357 | ) 358 | ); 359 | }); 360 | } 361 | 362 | // main render of stepzilla container 363 | 364 | }, { 365 | key: 'render', 366 | value: function render() { 367 | var _this6 = this; 368 | 369 | var compToRender = void 0; 370 | 371 | // clone the step component dynamically and tag it as activeComponent so we can validate it on next. also bind the jumpToStep piping method 372 | var cloneExtensions = { 373 | jumpToStep: function jumpToStep(t) { 374 | _this6.jumpToStep(t); 375 | } 376 | }; 377 | 378 | var componentPointer = this.props.steps[this.state.compState].component; 379 | 380 | // can only update refs if its a regular React component (not a pure component), so lets check that 381 | if (componentPointer instanceof _react.Component || // unit test deteceted that instanceof Component can be in either of these locations so test both (not sure why this is the case) 382 | componentPointer.type && componentPointer.type.prototype instanceof _react.Component) { 383 | cloneExtensions.ref = 'activeComponent'; 384 | } 385 | 386 | compToRender = _react2.default.cloneElement(componentPointer, cloneExtensions); 387 | 388 | return _react2.default.createElement( 389 | 'div', 390 | { className: 'multi-step', onKeyDown: function onKeyDown(evt) { 391 | _this6.handleKeyDown(evt); 392 | } }, 393 | this.props.showSteps ? _react2.default.createElement( 394 | 'ol', 395 | { className: 'progtrckr' }, 396 | this.renderSteps() 397 | ) : _react2.default.createElement('span', null), 398 | compToRender, 399 | _react2.default.createElement( 400 | 'div', 401 | { style: this.props.showNavigation ? {} : this.hidden, className: 'footer-buttons' }, 402 | _react2.default.createElement( 403 | 'button', 404 | { style: this.state.showPreviousBtn ? {} : this.hidden, 405 | className: 'btn btn-prev btn-primary btn-lg pull-left', 406 | onClick: function onClick() { 407 | _this6.previous(); 408 | } }, 409 | 'Previous' 410 | ), 411 | _react2.default.createElement( 412 | 'button', 413 | { style: this.state.showNextBtn ? {} : this.hidden, 414 | className: 'btn btn-next btn-primary btn-lg pull-right', 415 | onClick: function onClick() { 416 | _this6.next(); 417 | } }, 418 | this.state.nextStepText 419 | ) 420 | ) 421 | ); 422 | } 423 | }]); 424 | 425 | return StepZilla; 426 | }(_react.Component); 427 | 428 | exports.default = StepZilla; 429 | 430 | 431 | StepZilla.defaultProps = { 432 | showSteps: true, 433 | showNavigation: true, 434 | stepsNavigation: true, 435 | prevBtnOnLastStep: true, 436 | dontValidate: false, 437 | preventEnterSubmission: false, 438 | startAtStep: 0, 439 | nextTextOnFinalActionStep: "Next", 440 | hocValidationAppliedTo: [] 441 | }; --------------------------------------------------------------------------------