react-tour-guide
Advanced tools
| /** | ||
| * @jsx React.DOM | ||
| */ | ||
| 'use strict'; | ||
| var React = require('react/addons'); | ||
| var Indicator = React.createClass({ | ||
| propTypes: { | ||
| cssPosition: React.PropTypes.string.isRequired, | ||
| size: React.PropTypes.oneOfType([ | ||
| React.PropTypes.number, | ||
| React.PropTypes.string | ||
| ]).isRequired, | ||
| xPos: React.PropTypes.oneOfType([ | ||
| React.PropTypes.number, | ||
| React.PropTypes.string | ||
| ]).isRequired, | ||
| yPos: React.PropTypes.oneOfType([ | ||
| React.PropTypes.number, | ||
| React.PropTypes.string | ||
| ]).isRequired, | ||
| handleIndicatorClick: React.PropTypes.func.isRequired | ||
| }, | ||
| getDefaultProps: function() { | ||
| return { | ||
| cssPosition: 'absolute', | ||
| size: '30px', | ||
| xPos: 0, | ||
| yPos: 0 | ||
| }; | ||
| }, | ||
| render: function() { | ||
| var styles = { | ||
| 'position': this.props.cssPosition === 'fixed' ? 'fixed' : 'absolute', | ||
| 'top': this.props.yPos, | ||
| 'left': this.props.xPos, | ||
| 'width': this.props.size, | ||
| 'height': this.props.size | ||
| }; | ||
| return ( | ||
| <div className="tour-indicator" style={styles} onClick={this.props.handleIndicatorClick} /> | ||
| ); | ||
| } | ||
| }); | ||
| module.exports = React.createFactory(Indicator); |
+206
| /** | ||
| * @jsx React.DOM | ||
| */ | ||
| 'use strict'; | ||
| var React = require('react/addons'); | ||
| var $ = require('jquery'); | ||
| var Indicator = require('./Indicator'); | ||
| var Tooltip = require('./Tooltip'); | ||
| window.jQuery = $; | ||
| module.exports = function(settings, done) { | ||
| var mixin = { | ||
| settings: $.extend({ | ||
| scrollToSteps: true, | ||
| indicatorSize: 30, | ||
| tooltipWidth: 250, | ||
| steps: [] | ||
| }, settings), | ||
| completionCallback: done || function() {}, | ||
| getInitialState: function() { | ||
| return { | ||
| currentIndex: 0, | ||
| showTooltip: false | ||
| }; | ||
| }, | ||
| _renderLayer: function() { | ||
| // By calling this method in componentDidMount() and componentDidUpdate(), you're effectively | ||
| // creating a "wormhole" that funnels React's hierarchical updates through to a DOM node on an | ||
| // entirely different part of the page. | ||
| React.render(this.renderCurrentStep(), this._target); | ||
| }, | ||
| _unrenderLayer: function() { | ||
| React.unmountComponentAtNode(this._target); | ||
| }, | ||
| componentDidUpdate: function() { | ||
| if ( this.settings.steps[this.state.currentIndex] ) { | ||
| this._renderLayer(); | ||
| } else { | ||
| this.completionCallback(); | ||
| this._unrenderLayer(); | ||
| } | ||
| }, | ||
| componentDidMount: function() { | ||
| // Appending to the body is easier than managing the z-index of everything on the page. | ||
| // It's also better for accessibility and makes stacking a snap (since components will stack | ||
| // in mount order). | ||
| if ( this.settings.steps[this.state.currentIndex] ) { | ||
| this._target = document.createElement('div'); | ||
| document.body.appendChild(this._target); | ||
| this._renderLayer(); | ||
| } | ||
| }, | ||
| componentWillUnmount: function() { | ||
| this._unrenderLayer(); | ||
| document.body.removeChild(this._target); | ||
| }, | ||
| preventWindowOverflow: function(type, value, axis) { | ||
| var winWidth = parseInt($(window).width()); | ||
| var docHeight = parseInt($(document).height()); | ||
| var sizeVariable = type.toLowerCase() === 'indicator' ? this.settings.indicatorSize : this.settings.tooltipWidth; | ||
| if ( axis.toLowerCase() === 'x' ) { | ||
| if ( value + sizeVariable > winWidth ) { | ||
| value = winWidth - sizeVariable; | ||
| } else if ( value - sizeVariable/2 < 0 ) { | ||
| value = 0; | ||
| } | ||
| } else if ( axis.toLowerCase() === 'y' ) { | ||
| if ( value + sizeVariable/2 > docHeight ) { | ||
| value = docHeight - sizeVariable; | ||
| } else if ( value - sizeVariable/2 < 0 ) { | ||
| value = 0; | ||
| } | ||
| } | ||
| return value; | ||
| }, | ||
| calculatePlacement: function(step, $element) { | ||
| var offset = $element.offset(); | ||
| var width = $element.width(); | ||
| var height = $element.height(); | ||
| var position = step.position.toLowerCase(); | ||
| var topRegex = new RegExp('top', 'gi'); | ||
| var bottomRegex = new RegExp('bottom', 'gi'); | ||
| var leftRegex = new RegExp('left', 'gi'); | ||
| var rightRegex = new RegExp('right', 'gi'); | ||
| var placement = { | ||
| indicator: { | ||
| x: 0, | ||
| y: 0 | ||
| }, | ||
| tooltip: { | ||
| x: 0, | ||
| y: 0 | ||
| } | ||
| }; | ||
| // Calculate x positions | ||
| if ( leftRegex.test(position) ) { | ||
| placement.indicator.x = offset.left - this.settings.indicatorSize/2; | ||
| placement.tooltip.y = offset.top - this.settings.tooltipWidth/4; | ||
| } else if ( rightRegex.test(position) ) { | ||
| placement.indicator.x = offset.left + width - this.settings.indicatorSize/2; | ||
| placement.tooltip.x = offset.left + width - this.settings.tooltipWidth/2; | ||
| } else { | ||
| placement.indicator.x = offset.left + width/2 - this.settings.indicatorSize/2; | ||
| placement.tooltip.x = offset.left + width/2 - this.settings.tooltipWidth/2; | ||
| } | ||
| // Calculate y positions | ||
| if ( topRegex.test(position) ) { | ||
| placement.indicator.y = offset.top - this.settings.indicatorSize/2; | ||
| placement.tooltip.y = offset.top - this.settings.tooltipWidth/4; | ||
| } else if ( bottomRegex.test(position) ) { | ||
| placement.indicator.y = offset.top + height - this.settings.indicatorSize/2; | ||
| placement.tooltip.y = offset.top + height - this.settings.tooltipWidth/4; | ||
| } else { | ||
| placement.indicator.y = offset.top + height/2 - this.settings.indicatorSize/2; | ||
| placement.tooltip.y = offset.top + height/2 - this.settings.tooltipWidth/4; | ||
| } | ||
| return { | ||
| indicator: { | ||
| x: this.preventWindowOverflow('indicator', placement.indicator.x, 'x'), | ||
| y: this.preventWindowOverflow('indicator', placement.indicator.y, 'y') | ||
| }, | ||
| tooltip: { | ||
| x: this.preventWindowOverflow('tooltip', placement.tooltip.x, 'x'), | ||
| y: this.preventWindowOverflow('tooltip', placement.tooltip.y, 'y') | ||
| } | ||
| }; | ||
| }, | ||
| handleIndicatorClick: function(evt) { | ||
| evt.preventDefault(); | ||
| this.setState({ showTooltip: true }); | ||
| }, | ||
| closeTooltip: function(evt) { | ||
| evt.preventDefault(); | ||
| this.setState({ | ||
| showTooltip: false, | ||
| currentIndex: this.state.currentIndex + 1 | ||
| }, this.scrollToNextStep); | ||
| }, | ||
| scrollToNextStep: function() { | ||
| var $nextIndicator = $('.tour-indicator'); | ||
| if ( $nextIndicator && $nextIndicator.length && this.settings.scrollToSteps ) { | ||
| $('html, body').animate({ | ||
| 'scrollTop': $nextIndicator.offset().top - $(window).height()/2 | ||
| }, 500); | ||
| } | ||
| }, | ||
| renderCurrentStep: function() { | ||
| var element = null; | ||
| var currentStep = this.settings.steps[this.state.currentIndex]; | ||
| var $target = $(currentStep.element); | ||
| var cssPosition = $target.css('position'); | ||
| var placement = this.calculatePlacement(currentStep, $target); | ||
| if ( this.state.showTooltip ) { | ||
| element = ( | ||
| <Tooltip cssPosition={cssPosition} | ||
| xPos={placement.tooltip.x} | ||
| yPos={placement.tooltip.y} | ||
| width={this.settings.tooltipWidth} | ||
| text={currentStep.text} | ||
| closeTooltip={this.closeTooltip} /> | ||
| ); | ||
| } else { | ||
| element = ( | ||
| <Indicator cssPosition={cssPosition} | ||
| xPos={placement.indicator.x} | ||
| yPos={placement.indicator.y} | ||
| size={this.settings.indicatorSize} | ||
| handleIndicatorClick={this.handleIndicatorClick} /> | ||
| ); | ||
| } | ||
| return element; | ||
| } | ||
| }; | ||
| return mixin; | ||
| }; |
| /** | ||
| * @jsx React.DOM | ||
| */ | ||
| 'use strict'; | ||
| var React = require('react/addons'); | ||
| var Tooltip = React.createClass({ | ||
| propTypes: { | ||
| cssPosition: React.PropTypes.string.isRequired, | ||
| xPos: React.PropTypes.oneOfType([ | ||
| React.PropTypes.number, | ||
| React.PropTypes.string | ||
| ]).isRequired, | ||
| yPos: React.PropTypes.oneOfType([ | ||
| React.PropTypes.number, | ||
| React.PropTypes.string | ||
| ]).isRequired, | ||
| width: React.PropTypes.oneOfType([ | ||
| React.PropTypes.number, | ||
| React.PropTypes.string | ||
| ]).isRequired, | ||
| text: React.PropTypes.string.isRequired, | ||
| closeTooltip: React.PropTypes.func.isRequired | ||
| }, | ||
| getDefaultProps: function() { | ||
| return { | ||
| cssPosition: 'absolute', | ||
| xPos: 0, | ||
| yPos: 0, | ||
| text: '' | ||
| }; | ||
| }, | ||
| render: function() { | ||
| var styles = { | ||
| 'position': this.props.cssPosition === 'fixed' ? 'fixed' : 'absolute', | ||
| 'top': this.props.yPos, | ||
| 'left': this.props.xPos, | ||
| 'width': this.props.width | ||
| }; | ||
| return ( | ||
| <div> | ||
| <div className="tour-backdrop" onClick={this.props.closeTooltip} /> | ||
| <div className="tour-tooltip" style={styles}> | ||
| <p>{this.props.text}</p> | ||
| <div className="tour-btn close" onClick={this.props.closeTooltip}>Close</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| }); | ||
| module.exports = React.createFactory(Tooltip); |
| .tour-indicator { | ||
| height: 30px; | ||
| width: 30px; | ||
| -webkit-border-radius: 100%; | ||
| -moz-border-radius: 100%; | ||
| border-radius: 100%; | ||
| background-color: #3f94d0; | ||
| cursor: pointer; | ||
| /* Animate */ | ||
| -webkit-animation-name: pulse; | ||
| -webkit-animation-duration: 1.5s; | ||
| -webkit-animation-iteration-count: infinite; | ||
| -webkit-animation-timing-function: ease-out; | ||
| -moz-animation-name: pulse; | ||
| -moz-animation-duration: 1.5s; | ||
| -moz-animation-iteration-count: infinite; | ||
| -moz-animation-timing-function: ease-out; | ||
| -o-animation-name: pulse; | ||
| -o-animation-duration: 1.5s; | ||
| -o-animation-iteration-count: infinite; | ||
| -o-animation-timing-function: ease-out; | ||
| animation-name: pulse; | ||
| animation-duration: 1.5s; | ||
| animation-iteration-count: infinite; | ||
| animation-timing-function: ease-out; | ||
| } | ||
| /* WebKit/Safari and Chrome */ | ||
| @-webkit-keyframes pulse { | ||
| 0% { | ||
| -webkit-transform: scale(1); | ||
| transform: scale(1); | ||
| } | ||
| 80% { | ||
| -webkit-transform: scale(1.5); | ||
| transform: scale(1.5); | ||
| } | ||
| 100% { | ||
| -webkit-transform: scale(2.5); | ||
| transform: scale(2.5); | ||
| opacity: 0; | ||
| } | ||
| } | ||
| /* Gecko/Firefox */ | ||
| @-moz-keyframes pulse { | ||
| 0% { | ||
| -moz-transform: scale(0.3); | ||
| transform: scale(0.3); | ||
| opacity: 0.5; | ||
| } | ||
| 80% { | ||
| -moz-transform: scale(1.5); | ||
| transform: scale(1.5); | ||
| opacity: 0; | ||
| } | ||
| 100% { | ||
| -moz-transform: scale(2.5); | ||
| transform: scale(2.5); | ||
| opacity: 0; | ||
| } | ||
| } | ||
| /* Presto/Opera */ | ||
| @-o-keyframes pulse { | ||
| 0% { | ||
| -o-transform: scale(0.3); | ||
| transform: scale(0.3); | ||
| opacity: 0.5; | ||
| } | ||
| 80% { | ||
| -o-transform: scale(1.5); | ||
| transform: scale(1.5); | ||
| opacity: 0; | ||
| } | ||
| 100% { | ||
| -o-transform: scale(2.5); | ||
| transform: scale(2.5); | ||
| opacity: 0; | ||
| } | ||
| } | ||
| /* Standard */ | ||
| @keyframes pulse { | ||
| 0% { | ||
| transform: scale(0.3); | ||
| opacity: 0.5; | ||
| } | ||
| 80% { | ||
| transform: scale(1.5); | ||
| opacity: 0; | ||
| } | ||
| 100% { | ||
| transform: scale(2.5); | ||
| opacity: 0; | ||
| } | ||
| } | ||
| .tour-backdrop { | ||
| z-index: 998; /* one below z-index of tour-toolip (999) */ | ||
| position: fixed; | ||
| top: 0; | ||
| right: 0; | ||
| bottom: 0; | ||
| left: 0; | ||
| text-align: center; | ||
| background: rgba(0, 0, 0, 0.45); | ||
| } | ||
| .tour-tooltip { | ||
| width: 200px; | ||
| background-color: #ffffff; | ||
| color: #545454; | ||
| border: 1px solid #dddddd; | ||
| -webkit-border-radius: 4px; | ||
| -moz-border-radius: 4px; | ||
| border-radius: 4px; | ||
| padding: 5px; | ||
| -webkit-box-shadow: 0 8px 6px -6px rgba(0,0,0,0.4); | ||
| -moz-box-shadow: 0 8px 6px -6px rgba(0,0,0,0.4); | ||
| box-shadow: 0 8px 6px -6px rgba(0,0,0,0.4); | ||
| } | ||
| .tour-tooltip p { | ||
| margin: 5px 3px; | ||
| } | ||
| .tour-tooltip .tour-btn.close { | ||
| cursor: pointer; | ||
| display: block; | ||
| width: auto; | ||
| margin: 0 auto; | ||
| background-color: #ffffff; | ||
| border: 1px solid #dddddd; | ||
| -webkit-border-radius: 4px; | ||
| -moz-border-radius: 4px; | ||
| border-radius: 4px; | ||
| padding: 3px; | ||
| text-align: center; | ||
| } | ||
| .tour-tooltip .tour-btn.close:hover { | ||
| border-color: #3f94d0; | ||
| color: #3f94d0; | ||
| } |
+4
-3
@@ -0,1 +1,2 @@ | ||
| 'use strict'; | ||
@@ -5,8 +6,8 @@ | ||
| Mixin: require('./lib/Mixin'), | ||
| Mixin: require('./lib/js/Mixin'), | ||
| Indicator: require('./lib/Indicator'), | ||
| Indicator: require('./lib/js/Indicator'), | ||
| Tooltip: require('./lib/Tooltip') | ||
| Tooltip: require('./lib/js/Tooltip') | ||
| }; |
+7
-2
| { | ||
| "name": "react-tour-guide", | ||
| "version": "0.0.1", | ||
| "version": "0.0.2", | ||
| "author": "Jake Marsh <jakemmarsh@gmail.com>", | ||
@@ -15,7 +15,12 @@ "description": "A ReactJS mixin to give new users a popup-based tour of your application.", | ||
| "react", | ||
| "tour" | ||
| "tour", | ||
| "walkthrough", | ||
| "guide" | ||
| ], | ||
| "engines": { | ||
| "node": ">=0.12.x" | ||
| }, | ||
| "dependencies": { | ||
| "jquery": "^2.1.3" | ||
| } | ||
| } |
+47
-11
@@ -1,3 +0,3 @@ | ||
| react-tour-guide | ||
| ================= | ||
| react-tour-guide [](http://badge.fury.io/js/react-tour-guide) | ||
| ========================================================================================================================== | ||
@@ -15,10 +15,27 @@ A ReactJS mixin to give new users a popup-based tour of your application. | ||
| var TourGuideMixin = require('react-tour-guide').Mixin; | ||
| var tour = [ | ||
| var tour = { | ||
| scrollToSteps: true, | ||
| indicatorSize: 30, | ||
| tooltipWidth: 250, | ||
| steps: [ | ||
| { | ||
| text: 'This is the first step in the tour.', | ||
| element: 'header', | ||
| position: 'bottom' | ||
| }, | ||
| { | ||
| text: 'This is the second step in the tour.', | ||
| element: '.navigation', | ||
| position: 'right' | ||
| } | ||
| ] | ||
| }; | ||
| var cb = function() { | ||
| console.log('User has completed tour!'); | ||
| }; | ||
| ]; | ||
| var App = React.createClass({ | ||
| var InnerApp = React.createClass({ | ||
| mixins: [TourGuideMixin(tour, cb)], | ||
| mixins: [TourGuideMixin(tour)], | ||
| ... | ||
@@ -33,4 +50,10 @@ | ||
| An `array` of "steps" is passed to the TourGuideMixin. Each "step" represents one indicator and tooltip that a user must click through in the guided tour. A step has the following structure: | ||
| A Javascript object is passed to the `TourGuideMixin` to specify options, as well as the steps of your tour as an array. The options are: | ||
| - `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`. | ||
| - `indicatorSize` (int): the size (in pixels) that you want the Indicator to be. Defaults to `30`. | ||
| - `tooltipWidth` (int): the width (in pixels) that you want each tooltip to be. Defaults to `250`. | ||
| 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: | ||
| ```json | ||
@@ -48,4 +71,12 @@ { | ||
| ### HTML Structure | ||
| ### Completion Callback | ||
| 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. | ||
| --- | ||
| ### Styling | ||
| Some basic styling is provided in `/lib/styles/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. | ||
| 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. | ||
@@ -56,3 +87,3 @@ | ||
| ```html | ||
| <div /> | ||
| <div class="tour-indicator"></div> | ||
| ``` | ||
@@ -63,3 +94,8 @@ | ||
| ```html | ||
| <div /> | ||
| <div class="tour-backdrop"> | ||
| <div class="tour-tooltip"> | ||
| <p>{The step's text goes here.}</p> | ||
| <div class="tour-btn close">Close</div> | ||
| </div> | ||
| </div> | ||
| ``` |
| /** | ||
| * @jsx React.DOM | ||
| */ | ||
| 'use strict'; | ||
| var React = require('react/addons'); | ||
| var Tooltip = React.createClass({ | ||
| render: function() { | ||
| return ( | ||
| <div /> | ||
| ); | ||
| } | ||
| }); | ||
| module.exports = React.createFactory(Tooltip); |
| 'use strict'; | ||
| module.exports = { | ||
| componentDidMount: function() { | ||
| } | ||
| }; |
| /** | ||
| * @jsx React.DOM | ||
| */ | ||
| 'use strict'; | ||
| var React = require('react/addons'); | ||
| var Tooltip = React.createClass({ | ||
| render: function() { | ||
| return ( | ||
| <div /> | ||
| ); | ||
| } | ||
| }); | ||
| module.exports = React.createFactory(Tooltip); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
17664
290.97%10
11.11%405
1127.27%96
60%1
Infinity%1
Infinity%+ Added
+ Added