├── .npmignore ├── index.js ├── .jshintrc ├── .gitignore ├── package.json ├── lib ├── js │ ├── Indicator.js │ ├── Tooltip.js │ └── Mixin.js └── styles │ └── tour-guide.css ├── LICENSE ├── gulpfile.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | gulpfile.js 3 | .jshintrc -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | Mixin: require('./dist/js/Mixin'), 6 | 7 | Indicator: require('./dist/js/Indicator'), 8 | 9 | Tooltip: require('./dist/js/Tooltip') 10 | 11 | }; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "noarg": true, 12 | "regexp": true, 13 | "undef": true, 14 | "unused": true, 15 | "strict": true, 16 | "trailing": true, 17 | "smarttabs": true, 18 | "newcap": false 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Distro files 31 | dist 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tour-guide", 3 | "version": "0.0.8", 4 | "author": "Jake Marsh ", 5 | "description": "A ReactJS mixin to give new users a popup-based tour of your application.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jakemmarsh/react-tour-guide.git" 9 | }, 10 | "private": false, 11 | "license": "MIT", 12 | "keywords": [ 13 | "ReactJS", 14 | "react", 15 | "tour", 16 | "walkthrough", 17 | "guide" 18 | ], 19 | "engines": { 20 | "node": ">=0.12.x" 21 | }, 22 | "dependencies": { 23 | "jquery": "^2.1.3", 24 | "react": "^0.13.1" 25 | }, 26 | "devDependencies": { 27 | "del": "^1.1.1", 28 | "gulp": "^3.8.11", 29 | "gulp-if": "^1.2.5", 30 | "gulp-react": "^3.0.0", 31 | "gulp-strip-debug": "^1.0.2", 32 | "run-sequence": "^1.0.2" 33 | }, 34 | "scripts": { 35 | "prepublish": "gulp prod" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/js/Indicator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Indicator = React.createClass({ 6 | 7 | propTypes: { 8 | cssPosition: React.PropTypes.string.isRequired, 9 | xPos: React.PropTypes.oneOfType([ 10 | React.PropTypes.number, 11 | React.PropTypes.string 12 | ]).isRequired, 13 | yPos: React.PropTypes.oneOfType([ 14 | React.PropTypes.number, 15 | React.PropTypes.string 16 | ]).isRequired, 17 | handleIndicatorClick: React.PropTypes.func.isRequired 18 | }, 19 | 20 | getDefaultProps: function() { 21 | return { 22 | cssPosition: 'absolute', 23 | xPos: -1000, 24 | yPos: -1000 25 | }; 26 | }, 27 | 28 | render: function() { 29 | var styles = { 30 | 'position': this.props.cssPosition === 'fixed' ? 'fixed' : 'absolute', 31 | 'top': this.props.yPos, 32 | 'left': this.props.xPos 33 | }; 34 | 35 | return ( 36 |
37 | ); 38 | } 39 | 40 | }); 41 | 42 | module.exports = Indicator; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jake Marsh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var del = require('del'); 5 | var react = require('gulp-react'); 6 | var runSequence = require('run-sequence'); 7 | var stripDebug = require('gulp-strip-debug'); 8 | var gulpif = require('gulp-if'); 9 | 10 | gulp.task('clean', function(cb) { 11 | 12 | return del(['./dist/css/*', './dist/js/*'], cb); 13 | 14 | }); 15 | 16 | gulp.task('styles', function() { 17 | 18 | return gulp.src('./lib/styles/**/*.css') 19 | .pipe(gulp.dest('./dist/css/')); 20 | 21 | }); 22 | 23 | gulp.task('scripts', function() { 24 | 25 | return gulp.src('./lib/js/**/*.js') 26 | .pipe(react()) 27 | .pipe(gulpif(global.isProd, stripDebug())) 28 | .pipe(gulp.dest('./dist/js/')); 29 | 30 | }); 31 | 32 | gulp.task('dev', function() { 33 | 34 | global.isProd = false; 35 | 36 | runSequence(['styles', 'scripts']); 37 | 38 | gulp.watch('./lib/js/**/*.js', ['scripts']); 39 | gulp.watch('./lib/styles/**/*.css', ['styles']); 40 | 41 | }); 42 | 43 | gulp.task('prod', ['clean'], function() { 44 | 45 | global.isProd = true; 46 | 47 | return runSequence(['styles', 'scripts']); 48 | 49 | }); -------------------------------------------------------------------------------- /lib/js/Tooltip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Tooltip = React.createClass({ 6 | 7 | propTypes: { 8 | cssPosition: React.PropTypes.string.isRequired, 9 | xPos: React.PropTypes.oneOfType([ 10 | React.PropTypes.number, 11 | React.PropTypes.string 12 | ]).isRequired, 13 | yPos: React.PropTypes.oneOfType([ 14 | React.PropTypes.number, 15 | React.PropTypes.string 16 | ]).isRequired, 17 | text: React.PropTypes.string.isRequired, 18 | closeButtonText: React.PropTypes.string, 19 | closeTooltip: React.PropTypes.func.isRequired 20 | }, 21 | 22 | getDefaultProps: function() { 23 | return { 24 | cssPosition: 'absolute', 25 | xPos: -1000, 26 | yPos: -1000, 27 | text: '' 28 | }; 29 | }, 30 | 31 | render: function() { 32 | var styles = { 33 | 'position': this.props.cssPosition === 'fixed' ? 'fixed' : 'absolute', 34 | 'top': this.props.yPos, 35 | 'left': this.props.xPos 36 | }; 37 | 38 | return ( 39 |
40 |
41 |
42 |

{this.props.text || ''}

43 |
44 | {this.props.closeButtonText || 'Close'} 45 |
46 |
47 |
48 | ); 49 | } 50 | 51 | }); 52 | 53 | module.exports = Tooltip; 54 | -------------------------------------------------------------------------------- /lib/styles/tour-guide.css: -------------------------------------------------------------------------------- 1 | .tour-indicator { 2 | height: 30px; 3 | width: 30px; 4 | -webkit-border-radius: 100%; 5 | -moz-border-radius: 100%; 6 | border-radius: 100%; 7 | 8 | background-color: #3f94d0; 9 | cursor: pointer; 10 | 11 | /* Animate */ 12 | -webkit-animation-name: pulse; 13 | -webkit-animation-duration: 1.5s; 14 | -webkit-animation-iteration-count: infinite; 15 | -webkit-animation-timing-function: ease-out; 16 | -moz-animation-name: pulse; 17 | -moz-animation-duration: 1.5s; 18 | -moz-animation-iteration-count: infinite; 19 | -moz-animation-timing-function: ease-out; 20 | -o-animation-name: pulse; 21 | -o-animation-duration: 1.5s; 22 | -o-animation-iteration-count: infinite; 23 | -o-animation-timing-function: ease-out; 24 | animation-name: pulse; 25 | animation-duration: 1.5s; 26 | animation-iteration-count: infinite; 27 | animation-timing-function: ease-out; 28 | } 29 | 30 | /* WebKit/Safari and Chrome */ 31 | @-webkit-keyframes pulse { 32 | 0% { 33 | -webkit-transform: scale(1); 34 | transform: scale(1); 35 | } 36 | 37 | 80% { 38 | -webkit-transform: scale(1.5); 39 | transform: scale(1.5); 40 | } 41 | 42 | 100% { 43 | -webkit-transform: scale(2.5); 44 | transform: scale(2.5); 45 | opacity: 0; 46 | } 47 | } 48 | /* Gecko/Firefox */ 49 | @-moz-keyframes pulse { 50 | 0% { 51 | -moz-transform: scale(0.3); 52 | transform: scale(0.3); 53 | opacity: 0.5; 54 | } 55 | 56 | 80% { 57 | -moz-transform: scale(1.5); 58 | transform: scale(1.5); 59 | opacity: 0; 60 | } 61 | 62 | 100% { 63 | -moz-transform: scale(2.5); 64 | transform: scale(2.5); 65 | opacity: 0; 66 | } 67 | } 68 | /* Presto/Opera */ 69 | @-o-keyframes pulse { 70 | 0% { 71 | -o-transform: scale(0.3); 72 | transform: scale(0.3); 73 | opacity: 0.5; 74 | } 75 | 76 | 80% { 77 | -o-transform: scale(1.5); 78 | transform: scale(1.5); 79 | opacity: 0; 80 | } 81 | 82 | 100% { 83 | -o-transform: scale(2.5); 84 | transform: scale(2.5); 85 | opacity: 0; 86 | } 87 | } 88 | /* Standard */ 89 | @keyframes pulse { 90 | 0% { 91 | transform: scale(0.3); 92 | opacity: 0.5; 93 | } 94 | 95 | 80% { 96 | transform: scale(1.5); 97 | opacity: 0; 98 | } 99 | 100 | 100% { 101 | transform: scale(2.5); 102 | opacity: 0; 103 | } 104 | } 105 | 106 | .tour-backdrop { 107 | z-index: 998; /* one below z-index of tour-toolip (999) */ 108 | position: fixed; 109 | top: 0; 110 | right: 0; 111 | bottom: 0; 112 | left: 0; 113 | 114 | text-align: center; 115 | background: rgba(0, 0, 0, 0.45); 116 | } 117 | 118 | .tour-tooltip { 119 | z-index: 999; 120 | width: 250px; 121 | background-color: #ffffff; 122 | color: #545454; 123 | border: 1px solid #dddddd; 124 | -webkit-border-radius: 4px; 125 | -moz-border-radius: 4px; 126 | border-radius: 4px; 127 | padding: 5px; 128 | -webkit-box-shadow: 0 8px 6px -6px rgba(0,0,0,0.4); 129 | -moz-box-shadow: 0 8px 6px -6px rgba(0,0,0,0.4); 130 | box-shadow: 0 8px 6px -6px rgba(0,0,0,0.4); 131 | } 132 | 133 | .tour-tooltip p { 134 | margin: 5px 3px; 135 | } 136 | 137 | .tour-tooltip .tour-btn.close { 138 | cursor: pointer; 139 | display: block; 140 | width: auto; 141 | margin: 0 auto; 142 | background-color: #ffffff; 143 | border: 1px solid #dddddd; 144 | -webkit-border-radius: 4px; 145 | -moz-border-radius: 4px; 146 | border-radius: 4px; 147 | padding: 3px; 148 | text-align: center; 149 | } 150 | 151 | .tour-tooltip .tour-btn.close:hover { 152 | border-color: #3f94d0; 153 | color: #3f94d0; 154 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-tour-guide [![npm version](https://badge.fury.io/js/react-tour-guide.svg)](http://badge.fury.io/js/react-tour-guide) 2 | ========================================================================================================================== 3 | 4 | A ReactJS mixin to give new users a popup-based tour of your application. An example can be seen [here](http://jakemmarsh.com/react-tour-guide/). 5 | 6 | --- 7 | 8 | ### Getting Started 9 | 10 | 1. `npm install --save react-tour-guide` 11 | 2. `var TourGuideMixin = require('react-tour-guide').Mixin` 12 | 13 | ```javascript 14 | var TourGuideMixin = require('react-tour-guide').Mixin; 15 | var tour = { 16 | startIndex: 0, 17 | scrollToSteps: true, 18 | steps: [ 19 | { 20 | text: 'This is the first step in the tour.', 21 | element: 'header', 22 | position: 'bottom', 23 | closeButtonText: 'Next' 24 | }, 25 | { 26 | text: 'This is the second step in the tour.', 27 | element: '.navigation', 28 | position: 'right' 29 | } 30 | ] 31 | }; 32 | var cb = function() { 33 | console.log('User has completed tour!'); 34 | }; 35 | 36 | var App = React.createClass({ 37 | 38 | mixins: [TourGuideMixin(tour, cb)], 39 | 40 | ... 41 | 42 | }); 43 | ``` 44 | 45 | If you're going to initialize the mixin without steps and add them later asynchronously, set your `startIndex` to a negative value 46 | ```javascript 47 | ... 48 | startIndex: -1, 49 | steps: [] 50 | ... 51 | 52 | ``` 53 | 54 | --- 55 | 56 | ### Options 57 | 58 | A Javascript object is passed to the `TourGuideMixin` to specify options, as well as the steps of your tour as an array (there is also a method to define these asynchronously, discussed below). The options are: 59 | 60 | - `startIndex` (int): the index from which to begin the steps of the tour. This can be retrieved and saved via `getUserTourProgress` (discussed below), in order to be specified when a user returns. Defaults to `0`. 61 | - `scrollToSteps` (bool): if true, the page will be automatically scrolled to the next indicator (if one exists) after a tooltip is dismissed. Defaults to `true`. 62 | - `steps` (array): the array of steps to be included in your tour. Defaults to an empty array. 63 | 64 | 65 | Each "step" in the array represents one indicator and tooltip that a user must click through in the guided tour. A step has the following structure: 66 | 67 | ```json 68 | { 69 | "text": "The helpful tip or information the user should read at this step.", 70 | "element": "A jQuery selector for the element which the step relates to.", 71 | "position": "Where to position the indicator in relation to the element.", 72 | "closeButtonText": "An optional string to be used as the text for the tooltip close button." 73 | } 74 | ``` 75 | 76 | Positions can be chosen from: `top-left`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, `left`, and `center`. This defaults to `center`. 77 | 78 | --- 79 | 80 | ### Completion Callback 81 | 82 | An optional callback may be passed as the second parameter to `TourGuideMixin`, which will be called once the current user has completed all the steps of your tour. 83 | 84 | --- 85 | 86 | ### Methods 87 | 88 | ##### `setTourSteps(steps, cb)` 89 | 90 | This function is intended to provide you with a method to asynchronously define your steps (if they need to be fetched from a database, etc.) It takes a list of steps (of the form discussed earlier), along with an optional callback function as parameters. **This will completely overwrite any existing steps or progress**. Once the state is updated, the callback function will be invoked. 91 | 92 | ##### `getUserTourProgress()` 93 | 94 | Upon including the mixin, this will be available to your component. At any point, this method can be called to retrieve information about the current user's progress through the guided tour. The object returned looks like this: 95 | 96 | ```json 97 | { 98 | "index": 2, 99 | "percentageComplete": 50, 100 | "step": { 101 | "text": "...", 102 | "element": "...", 103 | "position": "..." 104 | } 105 | } 106 | ``` 107 | This information can be used to save a user's progress upon navigation from the page or application, allowing you to return them back to their correct spot when they visit next using the `startIndex` option (discussed above). 108 | 109 | --- 110 | 111 | ### Styling 112 | 113 | Some basic styling is provided in `/dist/css/tour-guide.css`. This can either be included directly in your project, or used as a base for your own custom styles. Below, the HTML structure of the tour is also outlined for custom styling. 114 | 115 | The guided tour consists of two main elements for each step: an `indicator` and a `tooltip`. An indicator is a flashing element positioned on a specific element on the page, cueing the user to click. Upon click, the associated tooltip is triggered which the user must then read and dismiss. 116 | 117 | **Note:** Elements are dynamically positioned by initially setting their `top` and `left` CSS properties to `-1000px`. Once they have been initially rendered and measured, they are then positioned correctly. Animations on these CSS properties should be avoided. 118 | 119 | ##### Indicator 120 | 121 | ```html 122 |
123 | ``` 124 | 125 | ##### Tooltip 126 | 127 | ```html 128 |
129 |
130 |
131 |

{The step's text goes here.}

132 |
Close
133 |
134 |
135 | ``` 136 | -------------------------------------------------------------------------------- /lib/js/Mixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var $ = require('jquery'); 5 | 6 | var Indicator = require('./Indicator'); 7 | var Tooltip = require('./Tooltip'); 8 | 9 | module.exports = function(settings, done) { 10 | 11 | var mixin = { 12 | 13 | settings: $.extend({ 14 | startIndex: 0, 15 | scrollToSteps: true, 16 | steps: [] 17 | }, settings), 18 | 19 | completionCallback: done || function() {}, 20 | 21 | getInitialState: function() { 22 | return { 23 | currentIndex: this.settings.startIndex, 24 | showTooltip: false, 25 | xPos: -1000, 26 | yPos: -1000 27 | }; 28 | }, 29 | 30 | _renderLayer: function() { 31 | // By calling this method in componentDidMount() and componentDidUpdate(), you're effectively 32 | // creating a "wormhole" that funnels React's hierarchical updates through to a DOM node on an 33 | // entirely different part of the page. 34 | this.setState({ xPos: -1000, yPos: -1000 }); 35 | React.render(this.renderCurrentStep(), this._target); 36 | this.calculatePlacement(); 37 | }, 38 | 39 | _unrenderLayer: function() { 40 | React.unmountComponentAtNode(this._target); 41 | }, 42 | 43 | componentDidUpdate: function(prevProps, prevState) { 44 | var hasNewIndex = this.state.currentIndex !== prevState.currentIndex; 45 | var hasNewStep = !!this.settings.steps[this.state.currentIndex]; 46 | var hasSteps = this.settings.steps.length > 0; 47 | var hasNewX = this.state.xPos !== prevState.xPos; 48 | var hasNewY = this.state.yPos !== prevState.yPos; 49 | var didToggleTooltip = this.state.showTooltip && this.state.showTooltip !== prevState.showTooltip; 50 | 51 | if ( (hasNewIndex && hasNewStep) || didToggleTooltip || hasNewX || hasNewY ) { 52 | this._renderLayer(); 53 | } else if ( hasSteps && hasNewIndex && !hasNewStep ) { 54 | this.completionCallback(); 55 | this._unrenderLayer(); 56 | } 57 | }, 58 | 59 | componentDidMount: function() { 60 | // Appending to the body is easier than managing the z-index of everything on the page. 61 | // It's also better for accessibility and makes stacking a snap (since components will stack 62 | // in mount order). 63 | this._target = document.createElement('div'); 64 | document.body.appendChild(this._target); 65 | 66 | if ( this.settings.steps[this.state.currentIndex] ) { 67 | this._renderLayer(); 68 | } 69 | $(window).on('resize', this.calculatePlacement); 70 | }, 71 | 72 | componentWillUnmount: function() { 73 | this._unrenderLayer(); 74 | document.body.removeChild(this._target); 75 | $(window).off('resize', this.calculatePlacement); 76 | }, 77 | 78 | setTourSteps: function(steps, cb) { 79 | if (!(steps instanceof Array)) { 80 | return false; 81 | } 82 | cb = cb || function() {}; 83 | this.settings.steps = steps; 84 | 85 | this.setState({ 86 | currentIndex: this.state.currentIndex < 0 ? 0 : this.state.currentIndex, 87 | setTourSteps: steps.length 88 | }, cb); 89 | }, 90 | 91 | getUserTourProgress: function() { 92 | return { 93 | index: this.state.currentIndex, 94 | percentageComplete: (this.state.currentIndex/this.settings.steps.length)*100, 95 | step: this.settings.steps[this.state.currentIndex] 96 | }; 97 | }, 98 | 99 | preventWindowOverflow: function(value, axis, elWidth, elHeight) { 100 | var winWidth = parseInt($(window).width()); 101 | var docHeight = parseInt($(document).height()); 102 | 103 | if ( axis.toLowerCase() === 'x' ) { 104 | if ( value + elWidth > winWidth ) { 105 | console.log('right overflow. value:', value, 'elWidth:', elWidth); 106 | value = winWidth - elWidth; 107 | } else if ( value < 0 ) { 108 | console.log('left overflow. value:', value, 'elWidth:', elWidth); 109 | value = 0; 110 | } 111 | } else if ( axis.toLowerCase() === 'y' ) { 112 | if ( value + elHeight > docHeight ) { 113 | console.log('bottom overflow. value:', value, 'elHeight:', elHeight); 114 | value = docHeight - elHeight; 115 | } else if ( value < 0 ) { 116 | console.log('top overflow. value:', value, 'elHeight:', elHeight); 117 | value = 0; 118 | } 119 | } 120 | 121 | return value; 122 | }, 123 | 124 | calculatePlacement: function() { 125 | var step = this.settings.steps[this.state.currentIndex]; 126 | var $target = $(step.element); 127 | var offset = $target.offset(); 128 | var targetWidth = $target.outerWidth(); 129 | var targetHeight = $target.outerHeight(); 130 | var position = step.position.toLowerCase(); 131 | var topRegex = new RegExp('top', 'gi'); 132 | var bottomRegex = new RegExp('bottom', 'gi'); 133 | var leftRegex = new RegExp('left', 'gi'); 134 | var rightRegex = new RegExp('right', 'gi'); 135 | var $element = this.state.showTooltip ? $('.tour-tooltip') : $('.tour-indicator'); 136 | var elWidth = $element.outerWidth(); 137 | var elHeight = $element.outerHeight(); 138 | var placement = { 139 | x: -1000, 140 | y: -1000 141 | }; 142 | 143 | // Calculate x position 144 | if ( leftRegex.test(position) ) { 145 | placement.x = offset.left - elWidth/2; 146 | } else if ( rightRegex.test(position) ) { 147 | placement.x = offset.left + targetWidth - elWidth/2; 148 | } else { 149 | placement.x = offset.left + targetWidth/2 - elWidth/2; 150 | } 151 | 152 | // Calculate y position 153 | if ( topRegex.test(position) ) { 154 | placement.y = offset.top - elHeight/2; 155 | } else if ( bottomRegex.test(position) ) { 156 | placement.y = offset.top + targetHeight - elHeight/2; 157 | } else { 158 | placement.y = offset.top + targetHeight/2 - elHeight/2; 159 | } 160 | 161 | this.setState({ 162 | xPos: this.preventWindowOverflow(placement.x, 'x', elWidth, elHeight), 163 | yPos: this.preventWindowOverflow(placement.y, 'y', elWidth, elHeight) 164 | }); 165 | }, 166 | 167 | handleIndicatorClick: function(evt) { 168 | evt.preventDefault(); 169 | 170 | this.setState({ showTooltip: true }); 171 | }, 172 | 173 | closeTooltip: function(evt) { 174 | evt.preventDefault(); 175 | 176 | this.setState({ 177 | showTooltip: false, 178 | currentIndex: this.state.currentIndex + 1 179 | }, this.scrollToNextStep); 180 | }, 181 | 182 | scrollToNextStep: function() { 183 | var $nextIndicator = $('.tour-indicator'); 184 | 185 | if ( $nextIndicator && $nextIndicator.length && this.settings.scrollToSteps ) { 186 | $('html, body').animate({ 187 | 'scrollTop': $nextIndicator.offset().top - $(window).height()/2 188 | }, 500); 189 | } 190 | }, 191 | 192 | renderCurrentStep: function() { 193 | var element = null; 194 | var currentStep = this.settings.steps[this.state.currentIndex]; 195 | var $target = currentStep && currentStep.element ? $(currentStep.element) : null; 196 | var cssPosition = $target ? $target.css('position') : null; 197 | 198 | if ( $target && $target.length ) { 199 | if ( this.state.showTooltip ) { 200 | element = ( 201 | 207 | ); 208 | } else { 209 | element = ( 210 | 214 | ); 215 | } 216 | } 217 | 218 | return element; 219 | } 220 | 221 | }; 222 | 223 | return mixin; 224 | 225 | }; 226 | --------------------------------------------------------------------------------