Quick and easy way to build your product tours with Bootstrap Popovers for Bootstrap 3 and 4. Based on Bootstrap-Tour, but with many fixes and features added.
Quick and easy way to build your product tours with Bootstrap Popovers for Bootstrap 3 and 4.
Bootstrap Tourist (called “Tourist” from here) is a fork of Bootstrap Tour, a plugin to create product tours.
The original Bootstrap Tour was written in coffeescript, and had a number of open feature and bug fix requests in the github repo. Bootstrap Tourist is an in-progress effort to move Bootstrap Tour to native ES6, fix some issues and add some requested features. You can read more about why Bootstrap Tourist exists, and why it’s not a github fork anymore, here: https://github.com/sorich87/bootstrap-tour/issues/713
Tourist works with Bootstrap 3.4 and 4.3 (specify “framework” option), however the “standalone” non-Bootstrap version is not available
There are some bugs in earlier BS3 and BS4, and jquery versions, that cause problems with Tourist. Please use minimum Bootstrap 3.4.x or 4.3.x, jquery 3.3.1 to avoid. Earlier versions may work, but try for yourself.
If you are new to Bootstrap Tourist, and don’t have Bootstrap Tour working, it’s as simple as doing the following:
Simply include bootstrap-tourist.js and bootstrap-tourist.css into your page:
<link href="bootstrap-tourist.css" rel="stylesheet">
...
<script src="bootstrap-tourist.js"></script>
Next, set up and start your tour with some steps:
var tour = new Tour({
framework: 'bootstrap3', // or "bootstrap4" depending on your version of bootstrap
steps: [
{
element: '#my-element',
title: 'Title of my step',
content: 'Content of my step'
},
{
element: '#my-other-element',
title: 'Title of my step',
content: 'Content of my step'
}
]
});
// Start the tour - note, no call to .init() is required
tour.start();
NOTE: A minified version is not provided because the entire purpose of this repo is to enable fixes, features and native port to ES6. If you are uncomfortable with this, please use the original Bootstrap Tour!
If you already have a working tour using Bootstrap Tour, and you want to move to Tourist (because it has some fixes etc), perform the following steps:
framework: 'bootstrap4'
option to your initialization code.
const tour = new Tour({
name: 'tourist',
steps: [...steps go here...],
debug: true, // you may wish to turn on debug for the first run through
framework: 'bootstrap4', // set Tourist to use BS4 compatibility
});
tour.init()
- this is not requiredtour.start()
to start the tour, and optionally add a call to tour.restart()
to force restart the tourTourist now has documentation included in the repo under the /docs/
folder. Take a look!
Control flow from onNext()
/ onPrevious()
options:
Option is available per step or globally:
var tourSteps = [
{
element: "#inputBanana",
title: "Bananas!",
content: "Bananas are yellow, except when they're not",
onNext: function (tour) {
if ($("#inputBanana").val() !== "banana") {
// no banana? highlight the banana field
$("#inputBanana").css("background-color", "red");
// do not jump to the next tour step!
return false;
}
}
}
];
var tour = new Tour({
steps: tourSteps,
framework: "bootstrap3", // or "bootstrap4" depending on your version of bootstrap
buttonTexts: { // customize or localize button texts
nextButton: "go on",
endTourButton: "ok it's over",
},
onNext: function(tour) {
if (someVar = true) {
// force the tour to jump to slide 3
tour.goTo(3);
// Prevent default move to next step - important!
return false;
}
}
});
tour.init()
:tour.init()
. Call tour.start()
to start/resume the Tour from previous step. Call tour.restart()
to always start Tour from the first steptour.init()
was a redundant method that caused conflict with hidden Tour elements.tour.init()
will generate a warning in the console (thanks to @pau1phi11ips).
var tourSteps = [
{
element: function () {
return $(document).find(".something");
},
title: "Dynamic",
content: "Element found by function"
},
{
element: "#static",
title: "Static",
content: "Element found by static ID"
}
];
var tourSteps = [
{
element: "#myButton",
reflex: true,
reflexOnly: true,
title: "Click it",
content: "Click to continue, or you're stuck"
}
];
Call function when element is missing:
onElementUnavailable
is called.function(tour, stepNumber) {}
Option is available at global and per step levels:
Use it per step to have a step-specific error handler:
function tourStepBroken(tour, stepNumber) {
alert("Uhoh, the tour broke on the #btnMagic element");
}
var tourSteps = [
{
element: "#btnMagic",
onElementUnavailable: tourStepBroken,
title: "Hold my beer",
content: "now watch this"
}
];
Use it globally, and optionally override per step, to have a robust and comprehensive error handler:
function tourBroken(tour, stepNumber) {
alert('The default error handler: tour element is done broke on step number ' + stepNumber);
}
var tourSteps = [
{
element: "#btnThis",
// onElementUnavailable: COMMENTED OUT, therefore default global handler used
title: "Some button",
content: "Some content"
},
{
element: "#btnThat",
onElementUnavailable: function (tour, stepNumber) {
// override the default global handler for this step only
alert("The tour broke on #btnThat step");
},
title: "Another button",
content: "More content"
}
];
var tour = new Tour({
steps: tourSteps,
framework: "bootstrap3", // or "bootstrap4" depending on your version of bootstrap
onElementUnavailable: tourBroken, // default "element unavailable" handler for all tour steps
});
Customizable progress bar & text
Progress bar & progress text:
showProgressBar
: shows a bootstrap progress bar for tour progress at the top of the tour contentshowProgressText
: shows a textual progress (N/X, i.e.: 1/24 for slide 1 of 24) in the tour titleexample:
var tourSteps = [
{
element: "#inputBanana",
title: "Bananas!",
content: "Bananas are yellow, except when they're not",
},
{
element: "#inputOranges",
title: "Oranges!",
content: "Oranges are not bananas",
showProgressBar: false, // don't show the progress bar on this step only
showProgressText: false, // don't show the progress text on this step only
}
];
var tour = new Tour({
framework: "bootstrap3", // or "bootstrap4" depending on your version of bootstrap
steps: tourSteps,
showProgressBar: true, // default show progress bar
showProgressText: true, // default show progress text
});
Customize the progressbar/progress text:
getProgressBarHTML(percent)
getProgressTextHTML(stepNumber, percent, stepCount)
example:
var tourSteps = [
{
element: "#inputBanana",
title: "Bananas!",
content: "Bananas are yellow, except when they're not",
},
{
element: "#inputOranges",
title: "Oranges!",
content: "Oranges are not bananas",
getProgressBarHTML: function(percent) {
// override the global progress bar function for this step
return '<div>You\'re ' + percent + ' of the way through!</div>';
}
}
];
var tour = new Tour({
steps: tourSteps,
showProgressBar: true, // default show progress bar
showProgressText: true, // default show progress text
getProgressBarHTML: function(percent) {
// default progress bar for all steps. Return valid HTML to draw the progress bar you want
return '<div class="progress"><div class="progress-bar progress-bar-striped" role="progressbar" style="width: ' + percent + '%;"></div></div>';
},
getProgressTextHTML: function(stepNumber, percent, stepCount) {
// default progress text for all steps
return 'Slide ' + stepNumber + "/" + stepCount;
}
});
var tourSteps = [
{
element: "#btnMCHammer",
preventInteraction: true,
title: "Hammer Time",
content: "You can't touch this"
}
];
delayOnElement
is an object with the following:
delayOnElement: {
delayElement: "#waitForMe", // the element to wait to become visible, or the string literal "element" to use the step element, or a function
maxDelay: 2000 // optional milliseconds to wait/timeout for the element, before crapping out. If maxDelay is not specified, this is 2000ms by default,
}
var tourSteps = [
{
element: "#btnPrettyTransition",
delayOnElement: {
delayElement: "element" // use string literal "element" to wait for this step's element, i.e.: #btnPrettyTransition
},
title: "Ages",
content: "This button takes ages to appear"
},
{
element: "#btnPrettyTransition",
delayOnElement: {
delayElement: function() {
return $("#btnPrettyTransition"); // return a jquery object to wait for, in this case #btnPrettyTransition
}
},
title: "Function",
content: "This button takes ages to appear, we're waiting for an element returned by function"
},
{
element: "#inputUnrelated",
delayOnElement: {
delayElement: "#divStuff" // wait until DOM element "divStuff" is visible before showing this tour step against DOM element "inputUnrelated"
},
title: "Waiting",
content: "This input is nice, but you only see this step when the other div appears"
},
{
element: "#btnDontForgetThis",
delayOnElement: {
delayElement: "element", // use string literal "element" to wait for this step's element, i.e.: #btnDontForgetThis
maxDelay: 5000 // wait 5 seconds for it to appear before timing out
},
title: "Cool",
content: "Remember the onElementUnavailable option!",
onElementUnavailable: function(tour, stepNumber) {
// This will be called if btnDontForgetThis is not visible after 5 seconds
console.log("Well that went badly wrong");
}
},
];
Trigger when modal closes:
example:
var tour = new Tour({
steps: tourSteps,
framework: "bootstrap3", // or "bootstrap4" depending on your version of bootstrap
onModalHidden: function(tour, stepNumber) {
console.log("Well damn, this step's element was a modal, or inside a modal, and the modal just done got dismissed y'all. Moving to step 3.");
// move to step number 3
return 3;
},
});
var tour = new Tour({
steps: tourSteps,
onModalHidden: function(tour, stepNumber) {
if (validateSomeModalContent() == false) {
// The validation failed, user dismissed modal without properly taking actions.
// Show the modal again
showModalAgain();
// Instruct tour to stay on same step
return false;
} else {
// Content was valid. Return null or do nothing to instruct tour to continue to next step
}
},
});
Handle Dialogs and BootstrapDialog plugin better https://nakupanda.github.io/bootstrap3-dialog/
<div class="modal" id="myModal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
...blah...
</div>
</div>
</div>
FOR BOOTSTRAPDIALOG PLUGIN:
This plugin creates random UUIDs for the dialog DOM ID. You need to fix the ID to something you know. Do this:
dlg = new BootstrapDialog.confirm({
...all the options...
});
// BootstrapDialog gives a random GUID ID for dialog. Give it a proper one
$objModal = dlg.getModal();
$objModal.attr("id", "myModal");
dlg.setId("myModal");
Call onPreviouslyEnded
if tour.start()
is called for a tour that has previously ended:
example:
var tour = new Tour({
steps: [ ... ],
framework: "bootstrap3", // or "bootstrap4" depending on your version of bootstrap
onPreviouslyEnded: function(tour) {
console.log("Looks like this tour has already ended");
},
});
tour.start();
var tour = new Tour({
steps: tourSteps,
template: null, // template option is null by default. Tourist will use the appropriate template for the framework version, in this case BS3 as per next option
framework: "bootstrap3", // can be string literal "bootstrap3" or "bootstrap4"
});
var tour = new Tour({
steps: tourSteps,
framework: "bootstrap4", // can be string literal "bootstrap3" or "bootstrap4"
template: '<div class="popover" role="tooltip">....blah....</div>'
});
sanitizeWhitelist
: specify an object that will be merged with the Bootstrap Popover default whitelist. Use the same structure as the default Bootstrap whitelist.sanitizeFunction
: specify a function that will be used to sanitize Tour content, with the following signature: string function(content). Specifying a function for this option will cause sanitizeWhitelist to be ignored. Specifying anything other than a function for this option will be ignored, and sanitizeWhitelist will be useddata-someplugin1="..."
and data-somethingelse="..."
. Allow content to include a selectpicker.
var tour=new Tour({
steps: tourSteps,
sanitizeWhitelist: {
"button" : ["data-someplugin1", "data-somethingelse"], // allows <button data-someplugin1="abc", data-somethingelse="xyz">
"select" : [] // allows <select>
}
});
var tour = new Tour({
steps: tourSteps,
sanitizeFunction: function(stepContent) {
// Bypass Bootstrap sanitizer using custom function to clean the tour step content.
// stepContent will contain the content of the step, i.e.: tourSteps[n].content. You must
// clean this content to prevent XSS and other vulnerabilities. Use your own code or a lib like DOMPurify
return DOMPurify.sanitize(stepContent);
}
});
var tour = new Tour({
steps: tourSteps,
sanitizeFunction: function(stepContent) {
// POTENTIAL SECURITY RISK
// bypass Bootstrap sanitizer, perform no sanitization, tour step content will be exactly as templated in tourSteps.
return stepContent;
}
});
var tour = new Tour({
framework: "bootstrap3", // or "bootstrap4" depending on your version of bootstrap
steps: [ ..... ],
localization: {
buttonTexts: {
prevButton: 'Back',
nextButton: 'Go',
pauseButton: 'Wait',
resumeButton: 'Continue',
endTourButton: 'Ok, enough'
}
}
});
var tour = new Tour({
localization: {
buttonTexts: {
endTourButton: 'Adios muchachos'
}
}
});
showIfUnintendedOrphan
:onElementUnavailable
showIfUnintendedOrphan
will show the tour step as an orphan. This ensures your tour step will always be shown.delayOnElement
takes priority over showIfUnintendedOrphan
, i.e. if you specify both delayOnElement and showIfUnintendedOrphan, the delay will timeout before the step will be shown as an orphan.
var tourSteps = [
{
element: "#btnSomething",
showIfUnintendedOrphan: true,
title: "Always",
content: "This tour step will always appear, either against element btnSomething if it exists, or as an orphan if it doesn't"
},
{
element: "#btnSomethingElse",
showIfUnintendedOrphan: true,
delayOnElement: {
delayElement: "element" // use string literal "element" to wait for this step's element, i.e.: #btnSomethingElse
},
title: "Always after a delay",
content: "This tour step will always appear. If element btnSomethingElse doesn't exist, delayOnElement will wait until it exists. If delayOnElement times out, step will show as an orphan"
},
{
element: "#btnDoesntExist",
showIfUnintendedOrphan: true,
title: "Always",
content: "This tour step will always appear",
onElementUnavailable: function() {
console.log("this will never get called as showIfUnintendedOrphan will show step as an orphan");
}
},
];
`
backdropOptions
is an object structured as follows (these are the default options, used if you do not set this option in your tour):
backdropOptions: {
highlightOpacity: 0.8,
highlightColor: '#FFF',
backdropSibling: false,
animation: {
// can be string of css class or function signature: function(domElement, step) {}
backdropShow: function(domElement) {
domElement.fadeIn();
},
backdropHide: function(domElement) {
domElement.fadeOut("slow")
},
highlightShow: function(domElement, step) {
step.fnPositionHighlight();
domElement.fadeIn();
},
highlightTransition: "tour-highlight-animation",
highlightHide: function(domElement) {
domElement.fadeOut("slow");
}
},
}
backdropOptions.highlightOpacity
: the alpha value of the div used to highlight the step element. You can control how visible/occluded the element is.backdropOptions.highlightColor
: the hex color code for the highlight. Normally you will want to use a white highlight (#FFF). However if your step element has a dark or black background, you may want to use a black highlight (#000). Experiment with the best colors for your UX.backdropOptions.backdropSibling
: solves display issues when step.element is a child of an element with fixed position or zindex specifiedbackdropOptions.animation
: The options can be either string literals specifying a CSS class, or a function. The application of these features work in exactly the same way for all backdropOptions.animation options. These options apply as per the following:backdropShow
: when a previously hidden backdrop is shownbackdropHide
: when a previously visible backdrop is hiddenhighlightShow
: when step N did not have an element, and step N+1 does have an elementhighlightHide
: when step N has an element, and step N+1 does not have an elementhighlightTransition
: when both step N and step N+1 have an element, and the highlight is visibly moved from one to the other
.my-custom-animation {
-webkit-transition: all .5s ease-out;
-moz-transition: all .5s ease-out;
-ms-transition: all .5s ease-out;
-o-transition: all .5s ease-out;
transition: all .5s ease-out;
}
var tour = new Tour({
steps: [ ..... ],
backdropOptions: {
animation: {
backdropShow: "my-custom-animation"
},
}
});
$(backdropOptions element).addClass("my-custom-animation");
$(backdropOptions element).show(0, function() {
$(this).removeClass("my-custom-animation");
});
function(domElement, step) {
}
backdropOptions.animation.highlightShow
, then domElement will be the highlighting div. You must then correctly position and show this div over the step element.
step = {
element: // the actual step element, even if the tour uses a function for this step.element option
container: // the container option (string) as specified in the step or globally, to help you decide how to set up the transition
backdrop: // as per step option (bool),
preventInteraction: // as per step option (bool),
isOrphan: // whether is step is actually an orphan, because the element was wrong and showIfUnintendedOrphan == true or for some other reason (bool),
orphan: // as per step option (bool),
showIfUnintendedOrphan: // as per step option (bool),
duration: // as per step option (bool),
delay: // as per step option (bool),
fnPositionHighlight: // a helper function to position the highlight element
};
fnPositionHighlight
option is provided to make it easy for you to automatically position the highlight div on top of the tour step element. The function simply performs the following:
function fnPositionHighlight() {
$(DOMID_HIGHLIGHT).width(_stepElement.outerWidth())
.height(_stepElement.outerHeight())
.offset(_stepElement.offset());
}
function(domElement, step) {
// do whatever setup for your custom transition
step.fnPositionHighlight();
}
backdropOptions
:
var tourSteps = [
{
element: "#btnSomething",
title: "Default",
content: "This tour step will use the default Tour.backdropOptions settings"
},
{
orphan: true,
title: "Default",
content: "This tour step will use the per step transitions as specified"
backdropOptions: {
// specify some options and use the defaults for the rest
highlightColor: #000, // this step element has a black background, so this RGB looks better
animation: {
// use a function to manually hide the highlight - of course this will only apply
// because this step is an orphan == true, and therefore there is no highlight to hide
highlightHide: function(domElement, step) {
domElement.fadeOut("slow")
}
},
}
},
`
```js
var tour = new Tour({
steps: [ ..... ],
backdropOptions: {
// default for all Tour steps
}
});
```
I’m a self-taught C++/x86 asm coder from the 80’s and 90’s. I’m not a web developer, I only have basic html, js, jquery knowledge that I pretty much taught myself over a couple of weeks or so. This isn’t my full time job, or even my part time job. I inherited the need to fix some stuff in Bootstrap Tour, and I simply published my fixes on github. I never intended to take on maintenance of a product, or become responsible for it in any way. I even said this in the Tour repo when I published my first fixes.
All of that info is to set your expectations. Tourist works, and it’s been thoroughly tested, but I keep getting tripped up by the niceties of modern coding. Tourist is offered as a non-minified, simple download-and-drop-in tool for you to use if you want to. I will do my best to follow coding standards, publish to npm when I remember, keep to a coding style, follow semantic versioning and all that stuff that makes it easy for you to use this plugin. However please fully expect that:
As a side note, Bootstrap Tour was made in coffeescript (which I’d never heard of). So when I started working on Tourist, it was using a codebase without comments, odd transpilation structures and approaches, and much more. So if you’re looking at the source and scratching your head as to why something is done in a certain way - yes, welcome to my world :-)
Feel free to contribute with pull requests, bug reports or enhancement suggestions.
Code licensed under the MIT license. Documentation licensed under CC BY 3.0.