Your IP : 216.73.216.48
// jshint ignore: start
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module unless amdModuleId is set
define(["jquery","core/popper"], function (a0,b1) {
return (root['Tour'] = factory(a0,b1));
});
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require("jquery"),require("popper.js"));
} else {
root['Tour'] = factory(root["$"],root["Popper"]);
}
}(this, function ($, Popper) {
"use strict";
/**
* A Tour.
*
* @class Tour
* @param {object} config The configuration object.
*/
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; };
function Tour(config) {
this.init(config);
}
/**
* The name of the tour.
*
* @property {String} tourName
*/
Tour.prototype.tourName;
/**
* The name of the tour storage key.
*
* @property {String} storageKey
*/
Tour.prototype.storageKey;
/**
* The session storage object
*
* @property {Storage} storage
*/
Tour.prototype.storage;
/**
* The original configuration as passed into the constructor.
*
* @property {Object} originalConfiguration
*/
Tour.prototype.originalConfiguration;
/**
* The list of step listeners.
*
* @property {Array} listeners
*/
Tour.prototype.listeners;
/**
* The list of event handlers.
*
* @property {Object} eventHandlers
*/
Tour.prototype.eventHandlers;
/**
* The list of steps.
*
* @property {Object[]} steps
*/
Tour.prototype.steps;
/**
* The current step node.
*
* @property {jQuery} currentStepNode
*/
Tour.prototype.currentStepNode;
/**
* The current step number.
*
* @property {Number} currentStepNumber
*/
Tour.prototype.currentStepNumber;
/**
* The popper for the current step.
*
* @property {Popper} currentStepPopper
*/
Tour.prototype.currentStepPopper;
/**
* The config for the current step.
*
* @property {Object} currentStepConfig
*/
Tour.prototype.currentStepConfig;
/**
* The template content.
*
* @property {String} templateContent
*/
Tour.prototype.templateContent;
/**
* Initialise the tour.
*
* @method init
* @param {Object} config The configuration object.
* @chainable
*/
Tour.prototype.init = function (config) {
// Unset all handlers.
this.eventHandlers = {};
// Reset the current tour states.
this.reset();
// Store the initial configuration.
this.originalConfiguration = config || {};
// Apply configuration.
this.configure.apply(this, arguments);
try {
this.storage = window.sessionStorage;
this.storageKey = 'tourstate_' + this.tourName;
} catch (e) {
this.storage = false;
this.storageKey = '';
}
return this;
};
/**
* Reset the current tour state.
*
* @method reset
* @chainable
*/
Tour.prototype.reset = function () {
// Hide the current step.
this.hide();
// Unset all handlers.
this.eventHandlers = [];
// Unset all listeners.
this.resetStepListeners();
// Unset the original configuration.
this.originalConfiguration = {};
// Reset the current step number and list of steps.
this.steps = [];
// Reset the current step number.
this.currentStepNumber = 0;
return this;
};
/**
* Prepare tour configuration.
*
* @method configure
* @chainable
*/
Tour.prototype.configure = function (config) {
var _this = this;
if ((typeof config === 'undefined' ? 'undefined' : _typeof(config)) === 'object') {
// Tour name.
if (typeof config.tourName !== 'undefined') {
this.tourName = config.tourName;
}
// Set up eventHandlers.
if (config.eventHandlers) {
(function () {
var eventName = void 0;
for (eventName in config.eventHandlers) {
config.eventHandlers[eventName].forEach(function (handler) {
this.addEventHandler(eventName, handler);
}, _this);
}
})();
}
// Reset the step configuration.
this.resetStepDefaults(true);
// Configure the steps.
if (_typeof(config.steps) === 'object') {
this.steps = config.steps;
}
if (typeof config.template !== 'undefined') {
this.templateContent = config.template;
}
}
// Check that we have enough to start the tour.
this.checkMinimumRequirements();
return this;
};
/**
* Check that the configuration meets the minimum requirements.
*
* @method checkMinimumRequirements
* @chainable
*/
Tour.prototype.checkMinimumRequirements = function () {
// Need a tourName.
if (!this.tourName) {
throw new Error("Tour Name required");
}
// Need a minimum of one step.
if (!this.steps || !this.steps.length) {
throw new Error("Steps must be specified");
}
};
/**
* Reset step default configuration.
*
* @method resetStepDefaults
* @param {Boolean} loadOriginalConfiguration Whether to load the original configuration supplied with the Tour.
* @chainable
*/
Tour.prototype.resetStepDefaults = function (loadOriginalConfiguration) {
if (typeof loadOriginalConfiguration === 'undefined') {
loadOriginalConfiguration = true;
}
this.stepDefaults = {};
if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {
this.setStepDefaults({});
} else {
this.setStepDefaults(this.originalConfiguration.stepDefaults);
}
return this;
};
/**
* Set the step defaults.
*
* @method setStepDefaults
* @param {Object} stepDefaults The step defaults to apply to all steps
* @chainable
*/
Tour.prototype.setStepDefaults = function (stepDefaults) {
if (!this.stepDefaults) {
this.stepDefaults = {};
}
$.extend(this.stepDefaults, {
element: '',
placement: 'top',
delay: 0,
moveOnClick: false,
moveAfterTime: 0,
orphan: false,
direction: 1
}, stepDefaults);
return this;
};
/**
* Retrieve the current step number.
*
* @method getCurrentStepNumber
* @return {Integer} The current step number
*/
Tour.prototype.getCurrentStepNumber = function () {
return parseInt(this.currentStepNumber, 10);
};
/**
* Store the current step number.
*
* @method setCurrentStepNumber
* @param {Integer} stepNumber The current step number
* @chainable
*/
Tour.prototype.setCurrentStepNumber = function (stepNumber) {
this.currentStepNumber = stepNumber;
if (this.storage) {
try {
this.storage.setItem(this.storageKey, stepNumber);
} catch (e) {
if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
this.storage.removeItem(this.storageKey);
}
}
}
};
/**
* Get the next step number after the currently displayed step.
*
* @method getNextStepNumber
* @return {Integer} The next step number to display
*/
Tour.prototype.getNextStepNumber = function (stepNumber) {
if (typeof stepNumber === 'undefined') {
stepNumber = this.getCurrentStepNumber();
}
var nextStepNumber = stepNumber + 1;
// Keep checking the remaining steps.
while (nextStepNumber <= this.steps.length) {
if (this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber))) {
return nextStepNumber;
}
nextStepNumber++;
}
return null;
};
/**
* Get the previous step number before the currently displayed step.
*
* @method getPreviousStepNumber
* @return {Integer} The previous step number to display
*/
Tour.prototype.getPreviousStepNumber = function (stepNumber) {
if (typeof stepNumber === 'undefined') {
stepNumber = this.getCurrentStepNumber();
}
var previousStepNumber = stepNumber - 1;
// Keep checking the remaining steps.
while (previousStepNumber >= 0) {
if (this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber))) {
return previousStepNumber;
}
previousStepNumber--;
}
return null;
};
/**
* Is the step the final step number?
*
* @method isLastStep
* @param {Integer} stepNumber Step number to test
* @return {Boolean} Whether the step is the final step
*/
Tour.prototype.isLastStep = function (stepNumber) {
var nextStepNumber = this.getNextStepNumber(stepNumber);
return nextStepNumber === null;
};
/**
* Is the step the first step number?
*
* @method isFirstStep
* @param {Integer} stepNumber Step number to test
* @return {Boolean} Whether the step is the first step
*/
Tour.prototype.isFirstStep = function (stepNumber) {
var previousStepNumber = this.getPreviousStepNumber(stepNumber);
return previousStepNumber === null;
};
/**
* Is this step potentially visible?
*
* @method isStepPotentiallyVisible
* @param {Integer} stepNumber Step number to test
* @return {Boolean} Whether the step is the potentially visible
*/
Tour.prototype.isStepPotentiallyVisible = function (stepConfig) {
if (!stepConfig) {
// Without step config, there can be no step.
return false;
}
if (this.isStepActuallyVisible(stepConfig)) {
// If it is actually visible, it is already potentially visible.
return true;
}
if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {
// Orphan steps have no target. They are always visible.
return true;
}
if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {
// Only return true if the activated has not been used yet.
return true;
}
// Not theoretically, or actually visible.
return false;
};
/**
* Is this step actually visible?
*
* @method isStepActuallyVisible
* @param {Integer} stepNumber Step number to test
* @return {Boolean} Whether the step is actually visible
*/
Tour.prototype.isStepActuallyVisible = function (stepConfig) {
if (!stepConfig) {
// Without step config, there can be no step.
return false;
}
var target = this.getStepTarget(stepConfig);
if (target && target.length && target.is(':visible')) {
// Without a target, there can be no step.
return !!target.length;
}
return false;
};
/**
* Go to the next step in the tour.
*
* @method next
* @chainable
*/
Tour.prototype.next = function () {
return this.gotoStep(this.getNextStepNumber());
};
/**
* Go to the previous step in the tour.
*
* @method previous
* @chainable
*/
Tour.prototype.previous = function () {
return this.gotoStep(this.getPreviousStepNumber(), -1);
};
/**
* Go to the specified step in the tour.
*
* @method gotoStep
* @param {Integer} stepNumber The step number to display
* @chainable
*/
Tour.prototype.gotoStep = function (stepNumber, direction) {
if (stepNumber < 0) {
return this.endTour();
}
var stepConfig = this.getStepConfig(stepNumber);
if (stepConfig === null) {
return this.endTour();
}
return this._gotoStep(stepConfig, direction);
};
Tour.prototype._gotoStep = function (stepConfig, direction) {
if (!stepConfig) {
return this.endTour();
}
if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {
stepConfig.delayed = true;
window.setTimeout(this._gotoStep.bind(this), stepConfig.delay, stepConfig, direction);
return this;
} else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {
var fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
return this.gotoStep(this[fn](stepConfig.stepNumber), direction);
}
this.hide();
this.fireEventHandlers('beforeRender', stepConfig);
this.renderStep(stepConfig);
this.fireEventHandlers('afterRender', stepConfig);
return this;
};
/**
* Fetch the normalised step configuration for the specified step number.
*
* @method getStepConfig
* @param {Integer} stepNumber The step number to fetch configuration for
* @return {Object} The step configuration
*/
Tour.prototype.getStepConfig = function (stepNumber) {
if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {
return null;
}
// Normalise the step configuration.
var stepConfig = this.normalizeStepConfig(this.steps[stepNumber]);
// Add the stepNumber to the stepConfig.
stepConfig = $.extend(stepConfig, { stepNumber: stepNumber });
return stepConfig;
};
/**
* Normalise the supplied step configuration.
*
* @method normalizeStepConfig
* @param {Object} stepConfig The step configuration to normalise
* @return {Object} The normalised step configuration
*/
Tour.prototype.normalizeStepConfig = function (stepConfig) {
if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {
stepConfig.moveAfterClick = stepConfig.reflex;
}
if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {
stepConfig.target = stepConfig.element;
}
if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {
stepConfig.body = stepConfig.content;
}
stepConfig = $.extend({}, this.stepDefaults, stepConfig);
stepConfig = $.extend({}, {
attachTo: stepConfig.target,
attachPoint: 'after'
}, stepConfig);
if (stepConfig.attachTo) {
stepConfig.attachTo = $(stepConfig.attachTo).first();
}
return stepConfig;
};
/**
* Fetch the actual step target from the selector.
*
* This should not be called until after any delay has completed.
*
* @method getStepTarget
* @param {Object} stepConfig The step configuration
* @return {$}
*/
Tour.prototype.getStepTarget = function (stepConfig) {
if (stepConfig.target) {
return $(stepConfig.target);
}
return null;
};
/**
* Fire any event handlers for the specified event.
*
* @param {String} eventName The name of the event to handle
* @param {Object} data Any data to pass to the event
* @chainable
*/
Tour.prototype.fireEventHandlers = function (eventName, data) {
if (typeof this.eventHandlers[eventName] === 'undefined') {
return this;
}
this.eventHandlers[eventName].forEach(function (thisEvent) {
thisEvent.call(this, data);
}, this);
return this;
};
/**
* @method addEventHandler
* @param string eventName The name of the event to listen for
* @param function handler The event handler to call
*/
Tour.prototype.addEventHandler = function (eventName, handler) {
if (typeof this.eventHandlers[eventName] === 'undefined') {
this.eventHandlers[eventName] = [];
}
this.eventHandlers[eventName].push(handler);
return this;
};
/**
* Process listeners for the step being shown.
*
* @method processStepListeners
* @param {object} stepConfig The configuration for the step
* @chainable
*/
Tour.prototype.processStepListeners = function (stepConfig) {
this.listeners.push(
// Next/Previous buttons.
{
node: this.currentStepNode,
args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
}, {
node: this.currentStepNode,
args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
},
// Close and end tour buttons.
{
node: this.currentStepNode,
args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
},
// Click backdrop and hide tour.
{
node: $('[data-flexitour="backdrop"]'),
args: ['click', $.proxy(this.hide, this)]
},
// Keypresses.
{
node: $('body'),
args: ['keydown', $.proxy(this.handleKeyDown, this)]
});
if (stepConfig.moveOnClick) {
var targetNode = this.getStepTarget(stepConfig);
this.listeners.push({
node: targetNode,
args: ['click', $.proxy(function (e) {
if ($(e.target).parents('[data-flexitour="container"]').length === 0) {
// Ignore clicks when they are in the flexitour.
window.setTimeout($.proxy(this.next, this), 500);
}
}, this)]
});
}
this.listeners.forEach(function (listener) {
listener.node.on.apply(listener.node, listener.args);
});
return this;
};
/**
* Reset step listeners.
*
* @method resetStepListeners
* @chainable
*/
Tour.prototype.resetStepListeners = function () {
// Stop listening to all external handlers.
if (this.listeners) {
this.listeners.forEach(function (listener) {
listener.node.off.apply(listener.node, listener.args);
});
}
this.listeners = [];
return this;
};
/**
* The standard step renderer.
*
* @method renderStep
* @param {Object} stepConfig The step configuration of the step
* @chainable
*/
Tour.prototype.renderStep = function (stepConfig) {
// Store the current step configuration for later.
this.currentStepConfig = stepConfig;
this.setCurrentStepNumber(stepConfig.stepNumber);
// Fetch the template and convert it to a $ object.
var template = $(this.getTemplateContent());
// Title.
template.find('[data-placeholder="title"]').html(stepConfig.title);
// Body.
template.find('[data-placeholder="body"]').html(stepConfig.body);
// Is this the first step?
if (this.isFirstStep(stepConfig.stepNumber)) {
template.find('[data-role="previous"]').prop('disabled', true);
} else {
template.find('[data-role="previous"]').prop('disabled', false);
}
// Is this the final step?
if (this.isLastStep(stepConfig.stepNumber)) {
template.find('[data-role="next"]').prop('disabled', true);
} else {
template.find('[data-role="next"]').prop('disabled', false);
}
template.find('[data-role="previous"]').attr('role', 'button');
template.find('[data-role="next"]').attr('role', 'button');
template.find('[data-role="end"]').attr('role', 'button');
// Replace the template with the updated version.
stepConfig.template = template;
// Add to the page.
this.addStepToPage(stepConfig);
// Process step listeners after adding to the page.
// This uses the currentNode.
this.processStepListeners(stepConfig);
return this;
};
/**
* Getter for the template content.
*
* @method getTemplateContent
* @return {$}
*/
Tour.prototype.getTemplateContent = function () {
return $(this.templateContent).clone();
};
/**
* Helper to add a step to the page.
*
* @method addStepToPage
* @param {Object} stepConfig The step configuration of the step
* @chainable
*/
Tour.prototype.addStepToPage = function (stepConfig) {
var stepContent = stepConfig.template;
// Create the stepNode from the template data.
var currentStepNode = $('<span data-flexitour="container"></span>').html(stepConfig.template).hide();
// The scroll animation occurs on the body or html.
var animationTarget = $('body, html').stop(true, true);
if (this.isStepActuallyVisible(stepConfig)) {
var targetNode = this.getStepTarget(stepConfig);
targetNode.data('flexitour', 'target');
var zIndex = this.calculateZIndex(targetNode);
if (zIndex) {
stepConfig.zIndex = zIndex + 1;
}
if (stepConfig.zIndex) {
currentStepNode.css('zIndex', stepConfig.zIndex + 1);
}
// Add the backdrop.
this.positionBackdrop(stepConfig);
$(document.body).append(currentStepNode);
this.currentStepNode = currentStepNode;
// Ensure that the step node is positioned.
// Some situations mean that the value is not properly calculated without this step.
this.currentStepNode.css({
top: 0,
left: 0
});
animationTarget.animate({
scrollTop: this.calculateScrollTop(stepConfig)
}).promise().then(function () {
this.positionStep(stepConfig);
this.revealStep(stepConfig);
}.bind(this));
} else if (stepConfig.orphan) {
stepConfig.isOrphan = true;
// This will be appended to the body instead.
stepConfig.attachTo = $('body').first();
stepConfig.attachPoint = 'append';
// Add the backdrop.
this.positionBackdrop(stepConfig);
// This is an orphaned step.
currentStepNode.addClass('orphan');
// It lives in the body.
$(document.body).append(currentStepNode);
this.currentStepNode = currentStepNode;
this.currentStepNode.offset(this.calculateStepPositionInPage());
this.currentStepNode.css('position', 'fixed');
this.currentStepPopper = new Popper($('body'), this.currentStepNode[0], {
removeOnDestroy: true,
placement: stepConfig.placement + '-start',
arrowElement: '[data-role="arrow"]',
// Empty the modifiers. We've already placed the step and don't want it moved.
modifiers: {
hide: {
enabled: false
},
applyStyle: {
onLoad: null,
enabled: false
}
}
});
this.revealStep(stepConfig);
}
return this;
};
Tour.prototype.revealStep = function (stepConfig) {
// Fade the step in.
this.currentStepNode.fadeIn('', $.proxy(function () {
// Announce via ARIA.
this.announceStep(stepConfig);
// Focus on the current step Node.
this.currentStepNode.focus();
window.setTimeout($.proxy(function () {
// After a brief delay, focus again.
// There seems to be an issue with Jaws where it only reads the dialogue title initially.
// This second focus helps it to read the full dialogue.
if (this.currentStepNode) {
this.currentStepNode.focus();
}
}, this), 100);
}, this));
return this;
};
/**
* Helper to announce the step on the page.
*
* @method announceStep
* @param {Object} stepConfig The step configuration of the step
* @chainable
*/
Tour.prototype.announceStep = function (stepConfig) {
// Setup the step Dialogue as per:
// * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal
// * https://www.w3.org/TR/wai-aria-practices/#dialog_modal
// Generate an ID for the current step node.
var stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;
this.currentStepNode.attr('id', stepId);
var bodyRegion = this.currentStepNode.find('[data-placeholder="body"]').first();
bodyRegion.attr('id', stepId + '-body');
bodyRegion.attr('role', 'document');
var headerRegion = this.currentStepNode.find('[data-placeholder="title"]').first();
headerRegion.attr('id', stepId + '-title');
headerRegion.attr('aria-labelledby', stepId + '-body');
// Generally, a modal dialog has a role of dialog.
this.currentStepNode.attr('role', 'dialog');
this.currentStepNode.attr('tabindex', 0);
this.currentStepNode.attr('aria-labelledby', stepId + '-title');
this.currentStepNode.attr('aria-describedby', stepId + '-body');
// Configure ARIA attributes on the target.
var target = this.getStepTarget(stepConfig);
if (target) {
if (!target.attr('tabindex')) {
target.attr('tabindex', 0);
}
target.data('original-describedby', target.attr('aria-describedby')).attr('aria-describedby', stepId + '-body');
}
this.accessibilityShow(stepConfig);
return this;
};
/**
* Handle key down events.
*
* @method handleKeyDown
* @param {EventFacade} e
*/
Tour.prototype.handleKeyDown = function (e) {
var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button:enabled';
switch (e.keyCode) {
case 27:
this.endTour();
break;
// 9 == Tab - trap focus for items with a backdrop.
case 9:
// Tab must be handled on key up only in this instance.
(function () {
if (!this.currentStepConfig.hasBackdrop) {
// Trapping tab focus is only handled for those steps with a backdrop.
return;
}
// Find all tabbable locations.
var activeElement = $(document.activeElement);
var stepTarget = this.getStepTarget(this.currentStepConfig);
var tabbableNodes = $(tabbableSelector);
var dialogContainer = $('span[data-flexitour="container"]');
var currentIndex = void 0;
// Filter out element which is not belong to target section or dialogue.
if (stepTarget) {
tabbableNodes = tabbableNodes.filter(function (index, element) {
return stepTarget != null && (stepTarget.has(element).length || dialogContainer.has(element).length || stepTarget.is(element) || dialogContainer.is(element));
});
}
// Find index of focusing element.
tabbableNodes.each(function (index, element) {
if (activeElement.is(element)) {
currentIndex = index;
return false;
}
});
var nextIndex = void 0;
var nextNode = void 0;
var focusRelevant = void 0;
if (currentIndex != void 0) {
var direction = 1;
if (e.shiftKey) {
direction = -1;
}
nextIndex = currentIndex;
do {
nextIndex += direction;
nextNode = $(tabbableNodes[nextIndex]);
} while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));
if (nextNode.length) {
// A new f
focusRelevant = nextNode.closest(stepTarget).length;
focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;
} else {
// Unable to find the target somehow.
focusRelevant = false;
}
}
if (focusRelevant) {
nextNode.focus();
} else {
if (e.shiftKey) {
// Focus on the last tabbable node in the step.
this.currentStepNode.find(tabbableSelector).last().focus();
} else {
if (this.currentStepConfig.isOrphan) {
// Focus on the step - there is no target.
this.currentStepNode.focus();
} else {
// Focus on the step target.
stepTarget.focus();
}
}
}
e.preventDefault();
}).call(this);
break;
}
};
/**
* Start the current tour.
*
* @method startTour
* @param {Integer} startAt Which step number to start at. If not specified, starts at the last point.
* @chainable
*/
Tour.prototype.startTour = function (startAt) {
if (this.storage && typeof startAt === 'undefined') {
var storageStartValue = this.storage.getItem(this.storageKey);
if (storageStartValue) {
var storageStartAt = parseInt(storageStartValue, 10);
if (storageStartAt <= this.steps.length) {
startAt = storageStartAt;
}
}
}
if (typeof startAt === 'undefined') {
startAt = this.getCurrentStepNumber();
}
this.fireEventHandlers('beforeStart', startAt);
this.gotoStep(startAt);
this.fireEventHandlers('afterStart', startAt);
return this;
};
/**
* Restart the tour from the beginning, resetting the completionlag.
*
* @method restartTour
* @chainable
*/
Tour.prototype.restartTour = function () {
return this.startTour(0);
};
/**
* End the current tour.
*
* @method endTour
* @chainable
*/
Tour.prototype.endTour = function () {
this.fireEventHandlers('beforeEnd');
if (this.currentStepConfig) {
var previousTarget = this.getStepTarget(this.currentStepConfig);
if (previousTarget) {
if (!previousTarget.attr('tabindex')) {
previousTarget.attr('tabindex', '-1');
}
previousTarget.focus();
}
}
this.hide(true);
this.fireEventHandlers('afterEnd');
return this;
};
/**
* Hide any currently visible steps.
*
* @method hide
* @chainable
*/
Tour.prototype.hide = function (transition) {
this.fireEventHandlers('beforeHide');
if (this.currentStepNode && this.currentStepNode.length) {
this.currentStepNode.hide();
if (this.currentStepPopper) {
this.currentStepPopper.destroy();
}
}
// Restore original target configuration.
if (this.currentStepConfig) {
var target = this.getStepTarget(this.currentStepConfig);
if (target) {
if (target.data('original-labelledby')) {
target.attr('aria-labelledby', target.data('original-labelledby'));
}
if (target.data('original-describedby')) {
target.attr('aria-describedby', target.data('original-describedby'));
}
if (target.data('original-tabindex')) {
target.attr('tabindex', target.data('tabindex'));
}
}
// Clear the step configuration.
this.currentStepConfig = null;
}
var fadeTime = 0;
if (transition) {
fadeTime = 400;
}
// Remove the backdrop features.
$('[data-flexitour="step-background"]').remove();
$('[data-flexitour="step-backdrop"]').removeAttr('data-flexitour');
$('[data-flexitour="backdrop"]').fadeOut(fadeTime, function () {
$(this).remove();
});
// Remove aria-describedby and tabindex attributes.
if (this.currentStepNode && this.currentStepNode.length) {
var stepId = this.currentStepNode.attr('id');
if (stepId) {
var currentStepElement = '[aria-describedby="' + stepId + '-body"]';
$(currentStepElement).removeAttr('tabindex');
$(currentStepElement).removeAttr('aria-describedby');
}
}
// Reset the listeners.
this.resetStepListeners();
this.accessibilityHide();
this.fireEventHandlers('afterHide');
this.currentStepNode = null;
this.currentStepPopper = null;
return this;
};
/**
* Show the current steps.
*
* @method show
* @chainable
*/
Tour.prototype.show = function () {
// Show the current step.
var startAt = this.getCurrentStepNumber();
return this.gotoStep(startAt);
};
/**
* Return the current step node.
*
* @method getStepContainer
* @return {jQuery}
*/
Tour.prototype.getStepContainer = function () {
return $(this.currentStepNode);
};
/**
* Calculate scrollTop.
*
* @method calculateScrollTop
* @param {Object} stepConfig The step configuration of the step
* @return {Number}
*/
Tour.prototype.calculateScrollTop = function (stepConfig) {
var scrollTop = $(window).scrollTop();
var viewportHeight = $(window).height();
var targetNode = this.getStepTarget(stepConfig);
if (stepConfig.placement === 'top') {
// If the placement is top, center scroll at the top of the target.
scrollTop = targetNode.offset().top - viewportHeight / 2;
} else if (stepConfig.placement === 'bottom') {
// If the placement is bottom, center scroll at the bottom of the target.
scrollTop = targetNode.offset().top + targetNode.height() - viewportHeight / 2;
} else if (targetNode.height() <= viewportHeight * 0.8) {
// If the placement is left/right, and the target fits in the viewport, centre screen on the target
scrollTop = targetNode.offset().top - (viewportHeight - targetNode.height()) / 2;
} else {
// If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer
// and change step attachmentTarget to top+.
scrollTop = targetNode.offset().top - viewportHeight * 0.2;
}
// Never scroll over the top.
scrollTop = Math.max(0, scrollTop);
// Never scroll beyond the bottom.
scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);
return Math.ceil(scrollTop);
};
/**
* Calculate dialogue position for page middle.
*
* @method calculateScrollTop
* @return {Number}
*/
Tour.prototype.calculateStepPositionInPage = function () {
var viewportHeight = $(window).height();
var stepHeight = this.currentStepNode.height();
var viewportWidth = $(window).width();
var stepWidth = this.currentStepNode.width();
return {
top: Math.ceil((viewportHeight - stepHeight) / 2),
left: Math.ceil((viewportWidth - stepWidth) / 2)
};
};
/**
* Position the step on the page.
*
* @method positionStep
* @param {Object} stepConfig The step configuration of the step
* @chainable
*/
Tour.prototype.positionStep = function (stepConfig) {
var content = this.currentStepNode;
if (!content || !content.length) {
// Unable to find the step node.
return this;
}
var flipBehavior = void 0;
switch (stepConfig.placement) {
case 'left':
flipBehavior = ['left', 'right', 'top', 'bottom'];
break;
case 'right':
flipBehavior = ['right', 'left', 'top', 'bottom'];
break;
case 'top':
flipBehavior = ['top', 'bottom', 'right', 'left'];
break;
case 'bottom':
flipBehavior = ['bottom', 'top', 'right', 'left'];
break;
default:
flipBehavior = 'flip';
break;
}
var target = this.getStepTarget(stepConfig);
var config = {
placement: stepConfig.placement + '-start',
removeOnDestroy: true,
modifiers: {
flip: {
behaviour: flipBehavior
},
arrow: {
element: '[data-role="arrow"]'
}
},
onCreate: function onCreate(data) {
recalculateArrowPosition(data);
},
onUpdate: function onUpdate(data) {
recalculateArrowPosition(data);
}
};
var recalculateArrowPosition = function recalculateArrowPosition(data) {
var placement = data.placement.split('-')[0];
var isVertical = ['left', 'right'].indexOf(placement) !== -1;
var arrowElement = data.instance.popper.querySelector('[data-role="arrow"]');
var stepElement = $(data.instance.popper.querySelector('[data-role="flexitour-step"]'));
if (isVertical) {
var arrowHeight = parseFloat(window.getComputedStyle(arrowElement).height);
var arrowOffset = parseFloat(window.getComputedStyle(arrowElement).top);
var popperHeight = parseFloat(window.getComputedStyle(data.instance.popper).height);
var popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).top);
var popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));
var popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;
var arrowPos = arrowOffset + arrowHeight / 2;
var maxPos = popperHeight + popperOffset - popperBorderWidth - popperBorderRadiusWidth;
var minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;
if (arrowPos >= maxPos || arrowPos <= minPos) {
var newArrowPos = 0;
if (arrowPos > popperHeight / 2) {
newArrowPos = maxPos - arrowHeight;
} else {
newArrowPos = minPos + arrowHeight;
}
$(arrowElement).css('top', newArrowPos);
}
} else {
var arrowWidth = parseFloat(window.getComputedStyle(arrowElement).width);
var _arrowOffset = parseFloat(window.getComputedStyle(arrowElement).left);
var popperWidth = parseFloat(window.getComputedStyle(data.instance.popper).width);
var _popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).left);
var _popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));
var _popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;
var _arrowPos = _arrowOffset + arrowWidth / 2;
var _maxPos = popperWidth + _popperOffset - _popperBorderWidth - _popperBorderRadiusWidth;
var _minPos = _popperOffset + _popperBorderWidth + _popperBorderRadiusWidth;
if (_arrowPos >= _maxPos || _arrowPos <= _minPos) {
var _newArrowPos = 0;
if (_arrowPos > popperWidth / 2) {
_newArrowPos = _maxPos - arrowWidth;
} else {
_newArrowPos = _minPos + arrowWidth;
}
$(arrowElement).css('left', _newArrowPos);
}
}
};
var background = $('[data-flexitour="step-background"]');
if (background.length) {
target = background;
}
this.currentStepPopper = new Popper(target, content[0], config);
return this;
};
/**
* Add the backdrop.
*
* @method positionBackdrop
* @param {Object} stepConfig The step configuration of the step
* @chainable
*/
Tour.prototype.positionBackdrop = function (stepConfig) {
if (stepConfig.backdrop) {
this.currentStepConfig.hasBackdrop = true;
var backdrop = $('<div data-flexitour="backdrop"></div>');
if (stepConfig.zIndex) {
if (stepConfig.attachPoint === 'append') {
stepConfig.attachTo.append(backdrop);
} else {
backdrop.insertAfter(stepConfig.attachTo);
}
} else {
$('body').append(backdrop);
}
if (this.isStepActuallyVisible(stepConfig)) {
// The step has a visible target.
// Punch a hole through the backdrop.
var background = $('<div data-flexitour="step-background"></div>');
var targetNode = this.getStepTarget(stepConfig);
var buffer = 10;
var colorNode = targetNode;
if (buffer) {
colorNode = $('body');
}
background.css({
width: targetNode.outerWidth() + buffer + buffer,
height: targetNode.outerHeight() + buffer + buffer,
left: targetNode.offset().left - buffer,
top: targetNode.offset().top - buffer,
backgroundColor: this.calculateInherittedBackgroundColor(colorNode)
});
if (targetNode.offset().left < buffer) {
background.css({
width: targetNode.outerWidth() + targetNode.offset().left + buffer,
left: targetNode.offset().left
});
}
if (targetNode.offset().top < buffer) {
background.css({
height: targetNode.outerHeight() + targetNode.offset().top + buffer,
top: targetNode.offset().top
});
}
var targetRadius = targetNode.css('borderRadius');
if (targetRadius && targetRadius !== $('body').css('borderRadius')) {
background.css('borderRadius', targetRadius);
}
var targetPosition = this.calculatePosition(targetNode);
if (targetPosition === 'fixed') {
background.css('top', 0);
} else if (targetPosition === 'absolute') {
background.css('position', 'fixed');
}
var fader = background.clone();
fader.css({
backgroundColor: backdrop.css('backgroundColor'),
opacity: backdrop.css('opacity')
});
fader.attr('data-flexitour', 'step-background-fader');
if (stepConfig.zIndex) {
if (stepConfig.attachPoint === 'append') {
stepConfig.attachTo.append(background);
} else {
fader.insertAfter(stepConfig.attachTo);
background.insertAfter(stepConfig.attachTo);
}
} else {
$('body').append(fader);
$('body').append(background);
}
// Add the backdrop data to the actual target.
// This is the part which actually does the work.
targetNode.attr('data-flexitour', 'step-backdrop');
if (stepConfig.zIndex) {
backdrop.css('zIndex', stepConfig.zIndex);
background.css('zIndex', stepConfig.zIndex + 1);
targetNode.css('zIndex', stepConfig.zIndex + 2);
}
fader.fadeOut('2000', function () {
$(this).remove();
});
}
}
return this;
};
/**
* Calculate the inheritted z-index.
*
* @method calculateZIndex
* @param {jQuery} elem The element to calculate z-index for
* @return {Number} Calculated z-index
*/
Tour.prototype.calculateZIndex = function (elem) {
elem = $(elem);
while (elem.length && elem[0] !== document) {
// Ignore z-index if position is set to a value where z-index is ignored by the browser
// This makes behavior of this function consistent across browsers
// WebKit always returns auto if the element is positioned.
var position = elem.css("position");
if (position === "absolute" || position === "relative" || position === "fixed") {
// IE returns 0 when zIndex is not specified
// other browsers return a string
// we ignore the case of nested elements with an explicit value of 0
// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
var value = parseInt(elem.css("zIndex"), 10);
if (!isNaN(value) && value !== 0) {
return value;
}
}
elem = elem.parent();
}
return 0;
};
/**
* Calculate the inheritted background colour.
*
* @method calculateInherittedBackgroundColor
* @param {jQuery} elem The element to calculate colour for
* @return {String} Calculated background colour
*/
Tour.prototype.calculateInherittedBackgroundColor = function (elem) {
// Use a fake node to compare each element against.
var fakeNode = $('<div>').hide();
$('body').append(fakeNode);
var fakeElemColor = fakeNode.css('backgroundColor');
fakeNode.remove();
elem = $(elem);
while (elem.length && elem[0] !== document) {
var color = elem.css('backgroundColor');
if (color !== fakeElemColor) {
return color;
}
elem = elem.parent();
}
return null;
};
/**
* Calculate the inheritted position.
*
* @method calculatePosition
* @param {jQuery} elem The element to calculate position for
* @return {String} Calculated position
*/
Tour.prototype.calculatePosition = function (elem) {
elem = $(elem);
while (elem.length && elem[0] !== document) {
var position = elem.css('position');
if (position !== 'static') {
return position;
}
elem = elem.parent();
}
return null;
};
/**
* Perform accessibility changes for step shown.
*
* This will add aria-hidden="true" to all siblings and parent siblings.
*
* @method accessibilityShow
*/
Tour.prototype.accessibilityShow = function () {
var stateHolder = 'data-has-hidden';
var attrName = 'aria-hidden';
var hideFunction = function hideFunction(child) {
var flexitourRole = child.data('flexitour');
if (flexitourRole) {
switch (flexitourRole) {
case 'container':
case 'target':
return;
}
}
var hidden = child.attr(attrName);
if (!hidden) {
child.attr(stateHolder, true);
child.attr(attrName, true);
}
};
this.currentStepNode.siblings().each(function (index, node) {
hideFunction($(node));
});
this.currentStepNode.parentsUntil('body').siblings().each(function (index, node) {
hideFunction($(node));
});
};
/**
* Perform accessibility changes for step hidden.
*
* This will remove any newly added aria-hidden="true".
*
* @method accessibilityHide
*/
Tour.prototype.accessibilityHide = function () {
var stateHolder = 'data-has-hidden';
var attrName = 'aria-hidden';
var showFunction = function showFunction(child) {
var hidden = child.attr(stateHolder);
if (typeof hidden !== 'undefined') {
child.removeAttr(stateHolder);
child.removeAttr(attrName);
}
};
$('[' + stateHolder + ']').each(function (index, node) {
showFunction($(node));
});
};
if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object') {
module.exports = Tour;
}
return Tour;
}));