1 // jshint ignore: start
2 (function (root, factory) {
3 if (typeof define === 'function' && define.amd) {
4 // AMD. Register as an anonymous module unless amdModuleId is set
5 define(["jquery","./popper"], function (a0,b1) {
6 return (root['Tour'] = factory(a0,b1));
8 } else if (typeof exports === 'object') {
9 // Node. Does not work with strict CommonJS, but
10 // only CommonJS-like environments that support module.exports,
12 module.exports = factory(require("jquery"),require("popper.js"));
14 root['Tour'] = factory($,Popper);
16 }(this, function ($, Popper) {
24 * @param {object} config The configuration object.
27 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
29 function Tour(config) {
34 * The name of the tour.
36 * @property {String} tourName
38 Tour.prototype.tourName;
41 * The original configuration as passed into the constructor.
43 * @property {Object} originalConfiguration
45 Tour.prototype.originalConfiguration;
48 * The list of step listeners.
50 * @property {Array} listeners
52 Tour.prototype.listeners;
55 * The list of event handlers.
57 * @property {Object} eventHandlers
59 Tour.prototype.eventHandlers;
64 * @property {Object[]} steps
69 * The current step node.
71 * @property {jQuery} currentStepNode
73 Tour.prototype.currentStepNode;
76 * The current step number.
78 * @property {Number} currentStepNumber
80 Tour.prototype.currentStepNumber;
83 * The popper for the current step.
85 * @property {Popper} currentStepPopper
87 Tour.prototype.currentStepPopper;
90 * The config for the current step.
92 * @property {Object} currentStepConfig
94 Tour.prototype.currentStepConfig;
97 * The template content.
99 * @property {String} templateContent
101 Tour.prototype.templateContent;
104 * Initialise the tour.
107 * @param {Object} config The configuration object.
110 Tour.prototype.init = function (config) {
111 // Unset all handlers.
112 this.eventHandlers = {};
114 // Reset the current tour states.
117 // Store the initial configuration.
118 this.originalConfiguration = config || {};
120 // Apply configuration.
121 this.configure.apply(this, arguments);
127 * Reset the current tour state.
132 Tour.prototype.reset = function () {
133 // Hide the current step.
136 // Unset all handlers.
137 this.eventHandlers = [];
139 // Unset all listeners.
140 this.resetStepListeners();
142 // Unset the original configuration.
143 this.originalConfiguration = {};
145 // Reset the current step number and list of steps.
148 // Reset the current step number.
149 this.currentStepNumber = 0;
155 * Prepare tour configuration.
160 Tour.prototype.configure = function (config) {
163 if ((typeof config === 'undefined' ? 'undefined' : _typeof(config)) === 'object') {
165 if (typeof config.tourName !== 'undefined') {
166 this.tourName = config.tourName;
169 // Set up eventHandlers.
170 if (config.eventHandlers) {
172 var eventName = void 0;
173 for (eventName in config.eventHandlers) {
174 config.eventHandlers[eventName].forEach(function (handler) {
175 this.addEventHandler(eventName, handler);
181 // Reset the step configuration.
182 this.resetStepDefaults(true);
184 // Configure the steps.
185 if (_typeof(config.steps) === 'object') {
186 this.steps = config.steps;
189 if (typeof config.template !== 'undefined') {
190 this.templateContent = config.template;
194 // Check that we have enough to start the tour.
195 this.checkMinimumRequirements();
201 * Check that the configuration meets the minimum requirements.
203 * @method checkMinimumRequirements
206 Tour.prototype.checkMinimumRequirements = function () {
208 if (!this.tourName) {
209 throw new Error("Tour Name required");
212 // Need a minimum of one step.
213 if (!this.steps || !this.steps.length) {
214 throw new Error("Steps must be specified");
219 * Reset step default configuration.
221 * @method resetStepDefaults
222 * @param {Boolean} loadOriginalConfiguration Whether to load the original configuration supplied with the Tour.
225 Tour.prototype.resetStepDefaults = function (loadOriginalConfiguration) {
226 if (typeof loadOriginalConfiguration === 'undefined') {
227 loadOriginalConfiguration = true;
230 this.stepDefaults = {};
231 if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {
232 this.setStepDefaults({});
234 this.setStepDefaults(this.originalConfiguration.stepDefaults);
241 * Set the step defaults.
243 * @method setStepDefaults
244 * @param {Object} stepDefaults The step defaults to apply to all steps
247 Tour.prototype.setStepDefaults = function (stepDefaults) {
248 if (!this.stepDefaults) {
249 this.stepDefaults = {};
251 $.extend(this.stepDefaults, {
265 * Retrieve the current step number.
267 * @method getCurrentStepNumber
268 * @return {Integer} The current step number
270 Tour.prototype.getCurrentStepNumber = function () {
271 return parseInt(this.currentStepNumber, 10);
275 * Store the current step number.
277 * @method setCurrentStepNumber
278 * @param {Integer} stepNumber The current step number
281 Tour.prototype.setCurrentStepNumber = function (stepNumber) {
282 this.currentStepNumber = stepNumber;
286 * Get the next step number after the currently displayed step.
288 * @method getNextStepNumber
289 * @return {Integer} The next step number to display
291 Tour.prototype.getNextStepNumber = function (stepNumber) {
292 if (typeof stepNumber === 'undefined') {
293 stepNumber = this.getCurrentStepNumber();
295 var nextStepNumber = stepNumber + 1;
297 // Keep checking the remaining steps.
298 while (nextStepNumber <= this.steps.length) {
299 if (this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber))) {
300 return nextStepNumber;
309 * Get the previous step number before the currently displayed step.
311 * @method getPreviousStepNumber
312 * @return {Integer} The previous step number to display
314 Tour.prototype.getPreviousStepNumber = function (stepNumber) {
315 if (typeof stepNumber === 'undefined') {
316 stepNumber = this.getCurrentStepNumber();
318 var previousStepNumber = stepNumber - 1;
320 // Keep checking the remaining steps.
321 while (previousStepNumber >= 0) {
322 if (this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber))) {
323 return previousStepNumber;
325 previousStepNumber--;
332 * Is the step the final step number?
335 * @param {Integer} stepNumber Step number to test
336 * @return {Boolean} Whether the step is the final step
338 Tour.prototype.isLastStep = function (stepNumber) {
339 var nextStepNumber = this.getNextStepNumber(stepNumber);
341 return nextStepNumber === null;
345 * Is the step the first step number?
347 * @method isFirstStep
348 * @param {Integer} stepNumber Step number to test
349 * @return {Boolean} Whether the step is the first step
351 Tour.prototype.isFirstStep = function (stepNumber) {
352 var previousStepNumber = this.getPreviousStepNumber(stepNumber);
354 return previousStepNumber === null;
358 * Is this step potentially visible?
360 * @method isStepPotentiallyVisible
361 * @param {Integer} stepNumber Step number to test
362 * @return {Boolean} Whether the step is the potentially visible
364 Tour.prototype.isStepPotentiallyVisible = function (stepConfig) {
366 // Without step config, there can be no step.
370 if (this.isStepActuallyVisible(stepConfig)) {
371 // If it is actually visible, it is already potentially visible.
375 if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {
376 // Orphan steps have no target. They are always visible.
380 if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {
381 // Only return true if the activated has not been used yet.
385 // Not theoretically, or actually visible.
390 * Is this step actually visible?
392 * @method isStepActuallyVisible
393 * @param {Integer} stepNumber Step number to test
394 * @return {Boolean} Whether the step is actually visible
396 Tour.prototype.isStepActuallyVisible = function (stepConfig) {
398 // Without step config, there can be no step.
402 var target = this.getStepTarget(stepConfig);
403 if (target && target.length && target.is(':visible')) {
404 // Without a target, there can be no step.
405 return !!target.length;
412 * Go to the next step in the tour.
417 Tour.prototype.next = function () {
418 return this.gotoStep(this.getNextStepNumber());
422 * Go to the previous step in the tour.
427 Tour.prototype.previous = function () {
428 return this.gotoStep(this.getPreviousStepNumber(), -1);
432 * Go to the specified step in the tour.
435 * @param {Integer} stepNumber The step number to display
438 Tour.prototype.gotoStep = function (stepNumber, direction) {
439 if (stepNumber < 0) {
440 return this.endTour();
443 var stepConfig = this.getStepConfig(stepNumber);
444 if (stepConfig === null) {
445 return this.endTour();
448 return this._gotoStep(stepConfig, direction);
451 Tour.prototype._gotoStep = function (stepConfig, direction) {
453 return this.endTour();
456 if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {
457 stepConfig.delayed = true;
458 window.setTimeout(this._gotoStep.bind(this), stepConfig.delay, stepConfig, direction);
461 } else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {
462 var fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
463 return this.gotoStep(this[fn](stepConfig.stepNumber), direction);
468 this.fireEventHandlers('beforeRender', stepConfig);
469 this.renderStep(stepConfig);
470 this.fireEventHandlers('afterRender', stepConfig);
476 * Fetch the normalised step configuration for the specified step number.
478 * @method getStepConfig
479 * @param {Integer} stepNumber The step number to fetch configuration for
480 * @return {Object} The step configuration
482 Tour.prototype.getStepConfig = function (stepNumber) {
483 if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {
487 // Normalise the step configuration.
488 var stepConfig = this.normalizeStepConfig(this.steps[stepNumber]);
490 // Add the stepNumber to the stepConfig.
491 stepConfig = $.extend(stepConfig, { stepNumber: stepNumber });
497 * Normalise the supplied step configuration.
499 * @method normalizeStepConfig
500 * @param {Object} stepConfig The step configuration to normalise
501 * @return {Object} The normalised step configuration
503 Tour.prototype.normalizeStepConfig = function (stepConfig) {
505 if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {
506 stepConfig.moveAfterClick = stepConfig.reflex;
509 if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {
510 stepConfig.target = stepConfig.element;
513 if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {
514 stepConfig.body = stepConfig.content;
517 stepConfig = $.extend({}, this.stepDefaults, stepConfig);
519 stepConfig = $.extend({}, {
520 attachTo: stepConfig.target,
528 * Fetch the actual step target from the selector.
530 * This should not be called until after any delay has completed.
532 * @method getStepTarget
533 * @param {Object} stepConfig The step configuration
536 Tour.prototype.getStepTarget = function (stepConfig) {
537 if (stepConfig.target) {
538 return $(stepConfig.target);
545 * Fire any event handlers for the specified event.
547 * @param {String} eventName The name of the event to handle
548 * @param {Object} data Any data to pass to the event
551 Tour.prototype.fireEventHandlers = function (eventName, data) {
552 if (typeof this.eventHandlers[eventName] === 'undefined') {
556 this.eventHandlers[eventName].forEach(function (thisEvent) {
557 thisEvent.call(this, data);
564 * @method addEventHandler
565 * @param string eventName The name of the event to listen for
566 * @param function handler The event handler to call
568 Tour.prototype.addEventHandler = function (eventName, handler) {
569 if (typeof this.eventHandlers[eventName] === 'undefined') {
570 this.eventHandlers[eventName] = [];
573 this.eventHandlers[eventName].push(handler);
579 * Process listeners for the step being shown.
581 * @method processStepListeners
582 * @param {object} stepConfig The configuration for the step
585 Tour.prototype.processStepListeners = function (stepConfig) {
587 // Next/Previous buttons.
589 node: this.currentStepNode,
590 args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
592 node: this.currentStepNode,
593 args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
596 // Close and end tour buttons.
598 node: this.currentStepNode,
599 args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
605 args: ['keydown', $.proxy(this.handleKeyDown, this)]
608 if (stepConfig.moveOnClick) {
609 var targetNode = this.getStepTarget(stepConfig);
610 this.listeners.push({
612 args: ['click', $.proxy(function (e) {
613 if ($(e.target).parents('[data-flexitour="container"]').length === 0) {
614 // Ignore clicks when they are in the flexitour.
615 window.setTimeout($.proxy(this.next, this), 100);
621 this.listeners.forEach(function (listener) {
622 listener.node.on.apply(listener.node, listener.args);
629 * Reset step listeners.
631 * @method resetStepListeners
634 Tour.prototype.resetStepListeners = function () {
635 // Stop listening to all external handlers.
636 if (this.listeners) {
637 this.listeners.forEach(function (listener) {
638 listener.node.off.apply(listener.node, listener.args);
647 * The standard step renderer.
650 * @param {Object} stepConfig The step configuration of the step
653 Tour.prototype.renderStep = function (stepConfig) {
654 // Store the current step configuration for later.
655 this.currentStepConfig = stepConfig;
656 this.setCurrentStepNumber(stepConfig.stepNumber);
658 // Fetch the template and convert it to a $ object.
659 var template = $(this.getTemplateContent());
662 template.find('[data-placeholder="title"]').html(stepConfig.title);
665 template.find('[data-placeholder="body"]').html(stepConfig.body);
667 // Is this the first step?
668 if (this.isFirstStep(stepConfig.stepNumber)) {
669 template.find('[data-role="previous"]').prop('disabled', true);
671 template.find('[data-role="previous"]').prop('disabled', false);
674 // Is this the final step?
675 if (this.isLastStep(stepConfig.stepNumber)) {
676 template.find('[data-role="next"]').prop('disabled', true);
678 template.find('[data-role="next"]').prop('disabled', false);
681 template.find('[data-role="previous"]').attr('role', 'button');
682 template.find('[data-role="next"]').attr('role', 'button');
683 template.find('[data-role="end"]').attr('role', 'button');
685 // Replace the template with the updated version.
686 stepConfig.template = template;
689 this.addStepToPage(stepConfig);
691 // Process step listeners after adding to the page.
692 // This uses the currentNode.
693 this.processStepListeners(stepConfig);
699 * Getter for the template content.
701 * @method getTemplateContent
704 Tour.prototype.getTemplateContent = function () {
705 return $(this.templateContent).clone();
709 * Helper to add a step to the page.
711 * @method addStepToPage
712 * @param {Object} stepConfig The step configuration of the step
715 Tour.prototype.addStepToPage = function (stepConfig) {
716 var stepContent = stepConfig.template;
718 // Create the stepNode from the template data.
719 var currentStepNode = $('<span data-flexitour="container"></span>').html(stepConfig.template).hide();
721 // The scroll animation occurs on the body or html.
722 var animationTarget = $('body, html').stop(true, true);
724 if (this.isStepActuallyVisible(stepConfig)) {
725 var zIndex = this.calculateZIndex(this.getStepTarget(stepConfig));
727 stepConfig.zIndex = zIndex + 1;
730 if (stepConfig.zIndex) {
731 currentStepNode.css('zIndex', stepConfig.zIndex + 1);
735 this.positionBackdrop(stepConfig);
737 if (stepConfig.attachPoint === 'append') {
738 $(stepConfig.attachTo).append(currentStepNode);
739 this.currentStepNode = currentStepNode;
741 this.currentStepNode = currentStepNode.insertAfter($(stepConfig.attachTo));
744 // Ensure that the step node is positioned.
745 // Some situations mean that the value is not properly calculated without this step.
746 this.currentStepNode.css({
751 animationTarget.animate({
752 scrollTop: this.calculateScrollTop(stepConfig)
753 }).promise().then($.proxy(function () {
754 this.positionStep(stepConfig);
755 this.revealStep(stepConfig);
757 } else if (stepConfig.orphan) {
758 stepConfig.isOrphan = true;
760 // This will be appended to the body instead.
761 stepConfig.attachTo = 'body';
762 stepConfig.attachPoint = 'append';
765 this.positionBackdrop(stepConfig);
767 // This is an orphaned step.
768 currentStepNode.addClass('orphan');
770 // It lives in the body.
771 $(stepConfig.attachTo).append(currentStepNode);
772 this.currentStepNode = currentStepNode;
774 this.currentStepNode.offset(this.calculateStepPositionInPage());
776 this.currentStepPopper = new Popper($('body'), this.currentStepNode[0], {
777 placement: stepConfig.placement + '-start',
778 arrowElement: '[data-role="arrow"]',
779 // Empty the modifiers. We've already placed the step and don't want it moved.
783 this.revealStep(stepConfig);
789 Tour.prototype.revealStep = function (stepConfig) {
791 this.currentStepNode.fadeIn('', $.proxy(function () {
792 // Announce via ARIA.
793 this.announceStep(stepConfig);
795 // Focus on the current step Node.
796 this.currentStepNode.focus();
797 window.setTimeout($.proxy(function () {
798 // After a brief delay, focus again.
799 // There seems to be an issue with Jaws where it only reads the dialogue title initially.
800 // This second focus causes it to read the full dialogue.
801 if (this.currentStepNode) {
802 this.currentStepNode.focus();
811 * Helper to announce the step on the page.
813 * @method announceStep
814 * @param {Object} stepConfig The step configuration of the step
817 Tour.prototype.announceStep = function (stepConfig) {
818 // Setup the step Dialogue as per:
819 // * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal
820 // * https://www.w3.org/TR/wai-aria-practices/#dialog_modal
822 // Generate an ID for the current step node.
823 var stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;
824 this.currentStepNode.attr('id', stepId);
826 var bodyRegion = this.currentStepNode.find('[data-placeholder="body"]').first();
827 bodyRegion.attr('id', stepId + '-body');
828 bodyRegion.attr('role', 'document');
830 var headerRegion = this.currentStepNode.find('[data-placeholder="title"]').first();
831 headerRegion.attr('id', stepId + '-title');
832 headerRegion.attr('aria-labelledby', stepId + '-body');
834 // Generally, a modal dialog has a role of dialog.
835 this.currentStepNode.attr('role', 'dialog');
836 this.currentStepNode.attr('tabindex', 0);
837 this.currentStepNode.attr('aria-labelledby', stepId + '-title');
838 this.currentStepNode.attr('aria-describedby', stepId + '-body');
840 // Configure ARIA attributes on the target.
841 var target = this.getStepTarget(stepConfig);
843 if (!target.attr('tabindex')) {
844 target.attr('tabindex', 0);
847 target.data('original-describedby', target.attr('aria-describedby')).attr('aria-describedby', stepId + '-body');
854 * Handle key down events.
856 * @method handleKeyDown
857 * @param {EventFacade} e
859 Tour.prototype.handleKeyDown = function (e) {
860 var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button';
866 // 9 == Tab - trap focus for items with a backdrop.
868 // Tab must be handled on key up only in this instance.
870 if (!this.currentStepConfig.hasBackdrop) {
871 // Trapping tab focus is only handled for those steps with a backdrop.
875 // Find all tabbable locations.
876 var activeElement = $(document.activeElement);
877 var stepTarget = this.getStepTarget(this.currentStepConfig);
878 var tabbableNodes = $(tabbableSelector);
879 var currentIndex = void 0;
880 tabbableNodes.filter(function (index, element) {
881 if (activeElement.is(element)) {
882 currentIndex = index;
887 var nextIndex = void 0;
888 var nextNode = void 0;
889 var focusRelevant = void 0;
895 nextIndex = currentIndex;
897 nextIndex += direction;
898 nextNode = $(tabbableNodes[nextIndex]);
899 } while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));
900 if (nextNode.length) {
902 focusRelevant = nextNode.closest(stepTarget).length;
903 focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;
905 // Unable to find the target somehow.
906 focusRelevant = false;
914 // Focus on the last tabbable node in the step.
915 this.currentStepNode.find(tabbableSelector).last().focus();
917 if (this.currentStepConfig.isOrphan) {
918 // Focus on the step - there is no target.
919 this.currentStepNode.focus();
921 // Focus on the step target.
933 * Start the current tour.
936 * @param {Integer} startAt Which step number to start at. If not specified, starts at the last point.
939 Tour.prototype.startTour = function (startAt) {
940 if (typeof startAt === 'undefined') {
941 startAt = this.getCurrentStepNumber();
944 this.fireEventHandlers('beforeStart', startAt);
945 this.gotoStep(startAt);
946 this.fireEventHandlers('afterStart', startAt);
952 * Restart the tour from the beginning, resetting the completionlag.
954 * @method restartTour
957 Tour.prototype.restartTour = function () {
958 return this.startTour(0);
962 * End the current tour.
967 Tour.prototype.endTour = function () {
968 this.fireEventHandlers('beforeEnd');
970 if (this.currentStepConfig) {
971 var previousTarget = this.getStepTarget(this.currentStepConfig);
972 if (previousTarget) {
973 if (!previousTarget.attr('tabindex')) {
974 previousTarget.attr('tabindex', '-1');
976 previousTarget.focus();
982 this.fireEventHandlers('afterEnd');
988 * Hide any currently visible steps.
993 Tour.prototype.hide = function (transition) {
994 this.fireEventHandlers('beforeHide');
996 if (this.currentStepNode && this.currentStepNode.length) {
997 this.currentStepNode.hide();
998 if (this.currentStepPopper) {
999 this.currentStepPopper.destroy();
1003 // Restore original target configuration.
1004 if (this.currentStepConfig) {
1005 var target = this.getStepTarget(this.currentStepConfig);
1007 if (target.data('original-labelledby')) {
1008 target.attr('aria-labelledby', target.data('original-labelledby'));
1011 if (target.data('original-describedby')) {
1012 target.attr('aria-describedby', target.data('original-describedby'));
1015 if (target.data('original-tabindex')) {
1016 target.attr('tabindex', target.data('tabindex'));
1020 // Clear the step configuration.
1021 this.currentStepConfig = null;
1029 // Remove the backdrop features.
1030 $('[data-flexitour="step-background"]').remove();
1031 $('[data-flexitour="step-backdrop"]').removeAttr('data-flexitour');
1032 $('[data-flexitour="backdrop"]').fadeOut(fadeTime, function () {
1036 // Reset the listeners.
1037 this.resetStepListeners();
1039 this.fireEventHandlers('afterHide');
1041 this.currentStepNode = null;
1042 this.currentStepPopper = null;
1047 * Show the current steps.
1052 Tour.prototype.show = function () {
1053 // Show the current step.
1054 var startAt = this.getCurrentStepNumber();
1056 return this.gotoStep(startAt);
1060 * Return the current step node.
1062 * @method getStepContainer
1065 Tour.prototype.getStepContainer = function () {
1066 return $(this.currentStepNode);
1070 * Calculate scrollTop.
1072 * @method calculateScrollTop
1073 * @param {Object} stepConfig The step configuration of the step
1076 Tour.prototype.calculateScrollTop = function (stepConfig) {
1077 var scrollTop = $(window).scrollTop();
1078 var viewportHeight = $(window).height();
1079 var targetNode = this.getStepTarget(stepConfig);
1081 if (stepConfig.placement === 'top') {
1082 // If the placement is top, center scroll at the top of the target.
1083 scrollTop = targetNode.offset().top - viewportHeight / 2;
1084 } else if (stepConfig.placement === 'bottom') {
1085 // If the placement is bottom, center scroll at the bottom of the target.
1086 scrollTop = targetNode.offset().top + targetNode.height() - viewportHeight / 2;
1087 } else if (targetNode.height() <= viewportHeight * 0.8) {
1088 // If the placement is left/right, and the target fits in the viewport, centre screen on the target
1089 scrollTop = targetNode.offset().top - (viewportHeight - targetNode.height()) / 2;
1091 // If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer
1092 // and change step attachmentTarget to top+.
1093 scrollTop = targetNode.offset().top - viewportHeight * 0.2;
1096 // Never scroll over the top.
1097 scrollTop = Math.max(0, scrollTop);
1099 // Never scroll beyond the bottom.
1100 scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);
1102 return Math.ceil(scrollTop);
1106 * Calculate dialogue position for page middle.
1108 * @method calculateScrollTop
1111 Tour.prototype.calculateStepPositionInPage = function () {
1112 var viewportHeight = $(window).height();
1113 var stepHeight = this.currentStepNode.height();
1114 var scrollTop = $(window).scrollTop();
1116 var viewportWidth = $(window).width();
1117 var stepWidth = this.currentStepNode.width();
1118 var scrollLeft = $(window).scrollLeft();
1121 top: Math.ceil(scrollTop + (viewportHeight - stepHeight) / 2),
1122 left: Math.ceil(scrollLeft + (viewportWidth - stepWidth) / 2)
1127 * Position the step on the page.
1129 * @method positionStep
1130 * @param {Object} stepConfig The step configuration of the step
1133 Tour.prototype.positionStep = function (stepConfig) {
1134 var content = this.currentStepNode;
1135 if (!content || !content.length) {
1136 // Unable to find the step node.
1140 var flipBehavior = void 0;
1141 switch (stepConfig.placement) {
1143 flipBehavior = ['left', 'right', 'top', 'bottom'];
1146 flipBehavior = ['right', 'left', 'top', 'bottom'];
1149 flipBehavior = ['top', 'bottom', 'right', 'left'];
1152 flipBehavior = ['bottom', 'top', 'right', 'left'];
1155 flipBehavior = 'flip';
1159 var target = this.getStepTarget(stepConfig);
1160 var background = $('[data-flexitour="step-background"]');
1161 if (background.length) {
1162 target = background;
1165 this.currentStepPopper = new Popper(target, content[0], {
1166 placement: stepConfig.placement + '-start',
1167 removeOnDestroy: true,
1168 flipBehavior: flipBehavior,
1169 arrowElement: '[data-role="arrow"]',
1170 modifiers: ['shift', 'offset', 'preventOverflow', 'keepTogether', this.centerPopper, 'arrow', 'flip', 'applyStyle']
1179 * @method positionBackdrop
1180 * @param {Object} stepConfig The step configuration of the step
1183 Tour.prototype.positionBackdrop = function (stepConfig) {
1184 if (stepConfig.backdrop) {
1185 this.currentStepConfig.hasBackdrop = true;
1186 var backdrop = $('<div data-flexitour="backdrop"></div>');
1188 if (stepConfig.zIndex) {
1189 if (stepConfig.attachPoint === 'append') {
1190 $(stepConfig.attachTo).append(backdrop);
1192 backdrop.insertAfter($(stepConfig.attachTo));
1195 $('body').append(backdrop);
1198 if (this.isStepActuallyVisible(stepConfig)) {
1199 // The step has a visible target.
1200 // Punch a hole through the backdrop.
1201 var background = $('<div data-flexitour="step-background"></div>');
1203 var targetNode = this.getStepTarget(stepConfig);
1207 var colorNode = targetNode;
1209 colorNode = $('body');
1213 width: targetNode.outerWidth() + buffer + buffer,
1214 height: targetNode.outerHeight() + buffer + buffer,
1215 left: targetNode.offset().left - buffer,
1216 top: targetNode.offset().top - buffer,
1217 backgroundColor: this.calculateInherittedBackgroundColor(colorNode)
1220 if (targetNode.offset().left < buffer) {
1222 width: targetNode.outerWidth() + targetNode.offset().left + buffer,
1223 left: targetNode.offset().left
1227 if (targetNode.offset().top < buffer) {
1229 height: targetNode.outerHeight() + targetNode.offset().top + buffer,
1230 top: targetNode.offset().top
1234 var targetRadius = targetNode.css('borderRadius');
1235 if (targetRadius && targetRadius !== $('body').css('borderRadius')) {
1236 background.css('borderRadius', targetRadius);
1239 var targetPosition = this.calculatePosition(targetNode);
1240 if (targetPosition === 'fixed') {
1241 background.css('top', 0);
1244 var fader = background.clone();
1246 backgroundColor: backdrop.css('backgroundColor'),
1247 opacity: backdrop.css('opacity')
1249 fader.attr('data-flexitour', 'step-background-fader');
1251 if (stepConfig.zIndex) {
1252 if (stepConfig.attachPoint === 'append') {
1253 $(stepConfig.attachTo).append(background);
1255 fader.insertAfter($(stepConfig.attachTo));
1256 background.insertAfter($(stepConfig.attachTo));
1259 $('body').append(fader);
1260 $('body').append(background);
1263 // Add the backdrop data to the actual target.
1264 // This is the part which actually does the work.
1265 targetNode.attr('data-flexitour', 'step-backdrop');
1267 if (stepConfig.zIndex) {
1268 backdrop.css('zIndex', stepConfig.zIndex);
1269 background.css('zIndex', stepConfig.zIndex + 1);
1270 targetNode.css('zIndex', stepConfig.zIndex + 2);
1273 fader.fadeOut('2000', function () {
1282 * Calculate the inheritted z-index.
1284 * @method calculateZIndex
1285 * @param {jQuery} elem The element to calculate z-index for
1286 * @return {Number} Calculated z-index
1288 Tour.prototype.calculateZIndex = function (elem) {
1290 while (elem.length && elem[0] !== document) {
1291 // Ignore z-index if position is set to a value where z-index is ignored by the browser
1292 // This makes behavior of this function consistent across browsers
1293 // WebKit always returns auto if the element is positioned.
1294 var position = elem.css("position");
1295 if (position === "absolute" || position === "relative" || position === "fixed") {
1296 // IE returns 0 when zIndex is not specified
1297 // other browsers return a string
1298 // we ignore the case of nested elements with an explicit value of 0
1299 // <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
1300 var value = parseInt(elem.css("zIndex"), 10);
1301 if (!isNaN(value) && value !== 0) {
1305 elem = elem.parent();
1312 * Calculate the inheritted background colour.
1314 * @method calculateInherittedBackgroundColor
1315 * @param {jQuery} elem The element to calculate colour for
1316 * @return {String} Calculated background colour
1318 Tour.prototype.calculateInherittedBackgroundColor = function (elem) {
1319 // Use a fake node to compare each element against.
1320 var fakeNode = $('<div>').hide();
1321 $('body').append(fakeNode);
1322 var fakeElemColor = fakeNode.css('backgroundColor');
1326 while (elem.length && elem[0] !== document) {
1327 var color = elem.css('backgroundColor');
1328 if (color !== fakeElemColor) {
1331 elem = elem.parent();
1338 * Calculate the inheritted position.
1340 * @method calculatePosition
1341 * @param {jQuery} elem The element to calculate position for
1342 * @return {String} Calculated position
1344 Tour.prototype.calculatePosition = function (elem) {
1346 while (elem.length && elem[0] !== document) {
1347 var position = elem.css('position');
1348 if (position !== 'static') {
1351 elem = elem.parent();
1357 Tour.prototype.centerPopper = function (data) {
1358 if (!this.isModifierRequired(Tour.prototype.centerPopper, this.modifiers.keepTogether)) {
1359 console.warn('WARNING: keepTogether modifier is required by centerPopper modifier in order to work, be sure to include it before arrow!');
1363 var placement = data.placement.split('-')[0];
1364 var reference = data.offsets.reference;
1365 var isVertical = ['left', 'right'].indexOf(placement) !== -1;
1367 var len = isVertical ? 'height' : 'width';
1368 var side = isVertical ? 'top' : 'left';
1370 data.offsets.popper[side] += Math.max(reference[len] / 2 - data.offsets.popper[len] / 2, 0);
1375 if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object') {
1376 module.exports = Tour;