46a0e19911065e0202f98546ff8b3143f0cc5fcf
[moodle.git] / admin / tool / usertours / amd / src / tour.js
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));
7     });
8   } else if (typeof exports === 'object') {
9     // Node. Does not work with strict CommonJS, but
10     // only CommonJS-like environments that support module.exports,
11     // like Node.
12     module.exports = factory(require("jquery"),require("popper.js"));
13   } else {
14     root['Tour'] = factory($,Popper);
15   }
16 }(this, function ($, Popper) {
18 "use strict";
20 /**
21  * A Tour.
22  *
23  * @class   Tour
24  * @param   {object}    config  The configuration object.
25  */
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) {
30     this.init(config);
31 }
33 /**
34  * The name of the tour.
35  *
36  * @property    {String}    tourName
37  */
38 Tour.prototype.tourName;
40 /**
41  * The original configuration as passed into the constructor.
42  *
43  * @property    {Object}    originalConfiguration
44  */
45 Tour.prototype.originalConfiguration;
47 /**
48  * The list of step listeners.
49  *
50  * @property    {Array}     listeners
51  */
52 Tour.prototype.listeners;
54 /**
55  * The list of event handlers.
56  *
57  * @property    {Object}    eventHandlers
58  */
59 Tour.prototype.eventHandlers;
61 /**
62  * The list of steps.
63  *
64  * @property    {Object[]}      steps
65  */
66 Tour.prototype.steps;
68 /**
69  * The current step node.
70  *
71  * @property    {jQuery}        currentStepNode
72  */
73 Tour.prototype.currentStepNode;
75 /**
76  * The current step number.
77  *
78  * @property    {Number}        currentStepNumber
79  */
80 Tour.prototype.currentStepNumber;
82 /**
83  * The popper for the current step.
84  *
85  * @property    {Popper}        currentStepPopper
86  */
87 Tour.prototype.currentStepPopper;
89 /**
90  * The config for the current step.
91  *
92  * @property    {Object}        currentStepConfig
93  */
94 Tour.prototype.currentStepConfig;
96 /**
97  * The template content.
98  *
99  * @property    {String}        templateContent
100  */
101 Tour.prototype.templateContent;
103 /**
104  * Initialise the tour.
105  *
106  * @method  init
107  * @param   {Object}    config  The configuration object.
108  * @chainable
109  */
110 Tour.prototype.init = function (config) {
111     // Unset all handlers.
112     this.eventHandlers = {};
114     // Reset the current tour states.
115     this.reset();
117     // Store the initial configuration.
118     this.originalConfiguration = config || {};
120     // Apply configuration.
121     this.configure.apply(this, arguments);
123     return this;
124 };
126 /**
127  * Reset the current tour state.
128  *
129  * @method  reset
130  * @chainable
131  */
132 Tour.prototype.reset = function () {
133     // Hide the current step.
134     this.hide();
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.
146     this.steps = [];
148     // Reset the current step number.
149     this.currentStepNumber = 0;
151     return this;
152 };
154 /**
155  * Prepare tour configuration.
156  *
157  * @method  configure
158  * @chainable
159  */
160 Tour.prototype.configure = function (config) {
161     var _this = this;
163     if ((typeof config === 'undefined' ? 'undefined' : _typeof(config)) === 'object') {
164         // Tour name.
165         if (typeof config.tourName !== 'undefined') {
166             this.tourName = config.tourName;
167         }
169         // Set up eventHandlers.
170         if (config.eventHandlers) {
171             (function () {
172                 var eventName = void 0;
173                 for (eventName in config.eventHandlers) {
174                     config.eventHandlers[eventName].forEach(function (handler) {
175                         this.addEventHandler(eventName, handler);
176                     }, _this);
177                 }
178             })();
179         }
181         // Reset the step configuration.
182         this.resetStepDefaults(true);
184         // Configure the steps.
185         if (_typeof(config.steps) === 'object') {
186             this.steps = config.steps;
187         }
189         if (typeof config.template !== 'undefined') {
190             this.templateContent = config.template;
191         }
192     }
194     // Check that we have enough to start the tour.
195     this.checkMinimumRequirements();
197     return this;
198 };
200 /**
201  * Check that the configuration meets the minimum requirements.
202  *
203  * @method  checkMinimumRequirements
204  * @chainable
205  */
206 Tour.prototype.checkMinimumRequirements = function () {
207     // Need a tourName.
208     if (!this.tourName) {
209         throw new Error("Tour Name required");
210     }
212     // Need a minimum of one step.
213     if (!this.steps || !this.steps.length) {
214         throw new Error("Steps must be specified");
215     }
216 };
218 /**
219  * Reset step default configuration.
220  *
221  * @method  resetStepDefaults
222  * @param   {Boolean}   loadOriginalConfiguration   Whether to load the original configuration supplied with the Tour.
223  * @chainable
224  */
225 Tour.prototype.resetStepDefaults = function (loadOriginalConfiguration) {
226     if (typeof loadOriginalConfiguration === 'undefined') {
227         loadOriginalConfiguration = true;
228     }
230     this.stepDefaults = {};
231     if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {
232         this.setStepDefaults({});
233     } else {
234         this.setStepDefaults(this.originalConfiguration.stepDefaults);
235     }
237     return this;
238 };
240 /**
241  * Set the step defaults.
242  *
243  * @method  setStepDefaults
244  * @param   {Object}    stepDefaults                The step defaults to apply to all steps
245  * @chainable
246  */
247 Tour.prototype.setStepDefaults = function (stepDefaults) {
248     if (!this.stepDefaults) {
249         this.stepDefaults = {};
250     }
251     $.extend(this.stepDefaults, {
252         element: '',
253         placement: 'top',
254         delay: 0,
255         moveOnClick: false,
256         moveAfterTime: 0,
257         orphan: false,
258         direction: 1
259     }, stepDefaults);
261     return this;
262 };
264 /**
265  * Retrieve the current step number.
266  *
267  * @method  getCurrentStepNumber
268  * @return  {Integer}                   The current step number
269  */
270 Tour.prototype.getCurrentStepNumber = function () {
271     return parseInt(this.currentStepNumber, 10);
272 };
274 /**
275  * Store the current step number.
276  *
277  * @method  setCurrentStepNumber
278  * @param   {Integer}   stepNumber      The current step number
279  * @chainable
280  */
281 Tour.prototype.setCurrentStepNumber = function (stepNumber) {
282     this.currentStepNumber = stepNumber;
283 };
285 /**
286  * Get the next step number after the currently displayed step.
287  *
288  * @method  getNextStepNumber
289  * @return  {Integer}    The next step number to display
290  */
291 Tour.prototype.getNextStepNumber = function (stepNumber) {
292     if (typeof stepNumber === 'undefined') {
293         stepNumber = this.getCurrentStepNumber();
294     }
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;
301         }
302         nextStepNumber++;
303     }
305     return null;
306 };
308 /**
309  * Get the previous step number before the currently displayed step.
310  *
311  * @method  getPreviousStepNumber
312  * @return  {Integer}    The previous step number to display
313  */
314 Tour.prototype.getPreviousStepNumber = function (stepNumber) {
315     if (typeof stepNumber === 'undefined') {
316         stepNumber = this.getCurrentStepNumber();
317     }
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;
324         }
325         previousStepNumber--;
326     }
328     return null;
329 };
331 /**
332  * Is the step the final step number?
333  *
334  * @method  isLastStep
335  * @param   {Integer}   stepNumber  Step number to test
336  * @return  {Boolean}               Whether the step is the final step
337  */
338 Tour.prototype.isLastStep = function (stepNumber) {
339     var nextStepNumber = this.getNextStepNumber(stepNumber);
341     return nextStepNumber === null;
342 };
344 /**
345  * Is the step the first step number?
346  *
347  * @method  isFirstStep
348  * @param   {Integer}   stepNumber  Step number to test
349  * @return  {Boolean}               Whether the step is the first step
350  */
351 Tour.prototype.isFirstStep = function (stepNumber) {
352     var previousStepNumber = this.getPreviousStepNumber(stepNumber);
354     return previousStepNumber === null;
355 };
357 /**
358  * Is this step potentially visible?
359  *
360  * @method  isStepPotentiallyVisible
361  * @param   {Integer}   stepNumber  Step number to test
362  * @return  {Boolean}               Whether the step is the potentially visible
363  */
364 Tour.prototype.isStepPotentiallyVisible = function (stepConfig) {
365     if (!stepConfig) {
366         // Without step config, there can be no step.
367         return false;
368     }
370     if (this.isStepActuallyVisible(stepConfig)) {
371         // If it is actually visible, it is already potentially visible.
372         return true;
373     }
375     if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {
376         // Orphan steps have no target. They are always visible.
377         return true;
378     }
380     if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {
381         // Only return true if the activated has not been used yet.
382         return true;
383     }
385     // Not theoretically, or actually visible.
386     return false;
387 };
389 /**
390  * Is this step actually visible?
391  *
392  * @method  isStepActuallyVisible
393  * @param   {Integer}   stepNumber  Step number to test
394  * @return  {Boolean}               Whether the step is actually visible
395  */
396 Tour.prototype.isStepActuallyVisible = function (stepConfig) {
397     if (!stepConfig) {
398         // Without step config, there can be no step.
399         return false;
400     }
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;
406     }
408     return false;
409 };
411 /**
412  * Go to the next step in the tour.
413  *
414  * @method  next
415  * @chainable
416  */
417 Tour.prototype.next = function () {
418     return this.gotoStep(this.getNextStepNumber());
419 };
421 /**
422  * Go to the previous step in the tour.
423  *
424  * @method  previous
425  * @chainable
426  */
427 Tour.prototype.previous = function () {
428     return this.gotoStep(this.getPreviousStepNumber(), -1);
429 };
431 /**
432  * Go to the specified step in the tour.
433  *
434  * @method  gotoStep
435  * @param   {Integer}   stepNumber      The step number to display
436  * @chainable
437  */
438 Tour.prototype.gotoStep = function (stepNumber, direction) {
439     if (stepNumber < 0) {
440         return this.endTour();
441     }
443     var stepConfig = this.getStepConfig(stepNumber);
444     if (stepConfig === null) {
445         return this.endTour();
446     }
448     return this._gotoStep(stepConfig, direction);
449 };
451 Tour.prototype._gotoStep = function (stepConfig, direction) {
452     if (!stepConfig) {
453         return this.endTour();
454     }
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);
460         return this;
461     } else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {
462         var fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
463         return this.gotoStep(this[fn](stepConfig.stepNumber), direction);
464     }
466     this.hide();
468     this.fireEventHandlers('beforeRender', stepConfig);
469     this.renderStep(stepConfig);
470     this.fireEventHandlers('afterRender', stepConfig);
472     return this;
473 };
475 /**
476  * Fetch the normalised step configuration for the specified step number.
477  *
478  * @method  getStepConfig
479  * @param   {Integer}   stepNumber      The step number to fetch configuration for
480  * @return  {Object}                    The step configuration
481  */
482 Tour.prototype.getStepConfig = function (stepNumber) {
483     if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {
484         return null;
485     }
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 });
493     return stepConfig;
494 };
496 /**
497  * Normalise the supplied step configuration.
498  *
499  * @method  normalizeStepConfig
500  * @param   {Object}    stepConfig      The step configuration to normalise
501  * @return  {Object}                    The normalised step configuration
502  */
503 Tour.prototype.normalizeStepConfig = function (stepConfig) {
505     if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {
506         stepConfig.moveAfterClick = stepConfig.reflex;
507     }
509     if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {
510         stepConfig.target = stepConfig.element;
511     }
513     if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {
514         stepConfig.body = stepConfig.content;
515     }
517     stepConfig = $.extend({}, this.stepDefaults, stepConfig);
519     stepConfig = $.extend({}, {
520         attachTo: stepConfig.target,
521         attachPoint: 'after'
522     }, stepConfig);
524     return stepConfig;
525 };
527 /**
528  * Fetch the actual step target from the selector.
529  *
530  * This should not be called until after any delay has completed.
531  *
532  * @method  getStepTarget
533  * @param   {Object}    stepConfig      The step configuration
534  * @return  {$}
535  */
536 Tour.prototype.getStepTarget = function (stepConfig) {
537     if (stepConfig.target) {
538         return $(stepConfig.target);
539     }
541     return null;
542 };
544 /**
545  * Fire any event handlers for the specified event.
546  *
547  * @param   {String}    eventName       The name of the event to handle
548  * @param   {Object}    data            Any data to pass to the event
549  * @chainable
550  */
551 Tour.prototype.fireEventHandlers = function (eventName, data) {
552     if (typeof this.eventHandlers[eventName] === 'undefined') {
553         return this;
554     }
556     this.eventHandlers[eventName].forEach(function (thisEvent) {
557         thisEvent.call(this, data);
558     }, this);
560     return this;
561 };
563 /**
564  * @method  addEventHandler
565  * @param   string      eventName       The name of the event to listen for
566  * @param   function    handler         The event handler to call
567  */
568 Tour.prototype.addEventHandler = function (eventName, handler) {
569     if (typeof this.eventHandlers[eventName] === 'undefined') {
570         this.eventHandlers[eventName] = [];
571     }
573     this.eventHandlers[eventName].push(handler);
575     return this;
576 };
578 /**
579  * Process listeners for the step being shown.
580  *
581  * @method  processStepListeners
582  * @param   {object}    stepConfig      The configuration for the step
583  * @chainable
584  */
585 Tour.prototype.processStepListeners = function (stepConfig) {
586     this.listeners.push(
587     // Next/Previous buttons.
588     {
589         node: this.currentStepNode,
590         args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
591     }, {
592         node: this.currentStepNode,
593         args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
594     },
596     // Close and end tour buttons.
597     {
598         node: this.currentStepNode,
599         args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
600     },
602     // Keypresses.
603     {
604         node: $('body'),
605         args: ['keydown', $.proxy(this.handleKeyDown, this)]
606     });
608     if (stepConfig.moveOnClick) {
609         var targetNode = this.getStepTarget(stepConfig);
610         this.listeners.push({
611             node: targetNode,
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);
616                 }
617             }, this)]
618         });
619     }
621     this.listeners.forEach(function (listener) {
622         listener.node.on.apply(listener.node, listener.args);
623     });
625     return this;
626 };
628 /**
629  * Reset step listeners.
630  *
631  * @method  resetStepListeners
632  * @chainable
633  */
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);
639         });
640     }
641     this.listeners = [];
643     return this;
644 };
646 /**
647  * The standard step renderer.
648  *
649  * @method  renderStep
650  * @param   {Object}    stepConfig      The step configuration of the step
651  * @chainable
652  */
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());
661     // Title.
662     template.find('[data-placeholder="title"]').html(stepConfig.title);
664     // Body.
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);
670     } else {
671         template.find('[data-role="previous"]').prop('disabled', false);
672     }
674     // Is this the final step?
675     if (this.isLastStep(stepConfig.stepNumber)) {
676         template.find('[data-role="next"]').prop('disabled', true);
677     } else {
678         template.find('[data-role="next"]').prop('disabled', false);
679     }
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;
688     // Add to the page.
689     this.addStepToPage(stepConfig);
691     // Process step listeners after adding to the page.
692     // This uses the currentNode.
693     this.processStepListeners(stepConfig);
695     return this;
696 };
698 /**
699  * Getter for the template content.
700  *
701  * @method  getTemplateContent
702  * @return  {$}
703  */
704 Tour.prototype.getTemplateContent = function () {
705     return $(this.templateContent).clone();
706 };
708 /**
709  * Helper to add a step to the page.
710  *
711  * @method  addStepToPage
712  * @param   {Object}    stepConfig      The step configuration of the step
713  * @chainable
714  */
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));
726         if (zIndex) {
727             stepConfig.zIndex = zIndex + 1;
728         }
730         if (stepConfig.zIndex) {
731             currentStepNode.css('zIndex', stepConfig.zIndex + 1);
732         }
734         // Add the backdrop.
735         this.positionBackdrop(stepConfig);
737         if (stepConfig.attachPoint === 'append') {
738             $(stepConfig.attachTo).append(currentStepNode);
739             this.currentStepNode = currentStepNode;
740         } else {
741             this.currentStepNode = currentStepNode.insertAfter($(stepConfig.attachTo));
742         }
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({
747             top: 0,
748             left: 0
749         });
751         animationTarget.animate({
752             scrollTop: this.calculateScrollTop(stepConfig)
753         }).promise().then($.proxy(function () {
754             this.positionStep(stepConfig);
755             this.revealStep(stepConfig);
756         }, this));
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';
764         // Add the backdrop.
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.
780             modifiers: []
781         });
783         this.revealStep(stepConfig);
784     }
786     return this;
787 };
789 Tour.prototype.revealStep = function (stepConfig) {
790     // Fade the step in.
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();
803             }
804         }, this), 100);
805     }, this));
807     return this;
808 };
810 /**
811  * Helper to announce the step on the page.
812  *
813  * @method  announceStep
814  * @param   {Object}    stepConfig      The step configuration of the step
815  * @chainable
816  */
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);
842     if (target) {
843         if (!target.attr('tabindex')) {
844             target.attr('tabindex', 0);
845         }
847         target.data('original-describedby', target.attr('aria-describedby')).attr('aria-describedby', stepId + '-body');
848     }
850     return this;
851 };
853 /**
854  * Handle key down events.
855  *
856  * @method  handleKeyDown
857  * @param   {EventFacade} e
858  */
859 Tour.prototype.handleKeyDown = function (e) {
860     var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button';
861     switch (e.keyCode) {
862         case 27:
863             this.endTour();
864             break;
866         // 9 == Tab - trap focus for items with a backdrop.
867         case 9:
868             // Tab must be handled on key up only in this instance.
869             (function () {
870                 if (!this.currentStepConfig.hasBackdrop) {
871                     // Trapping tab focus is only handled for those steps with a backdrop.
872                     return;
873                 }
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;
883                         return false;
884                     }
885                 });
887                 var nextIndex = void 0;
888                 var nextNode = void 0;
889                 var focusRelevant = void 0;
890                 if (currentIndex) {
891                     var direction = 1;
892                     if (e.shiftKey) {
893                         direction = -1;
894                     }
895                     nextIndex = currentIndex;
896                     do {
897                         nextIndex += direction;
898                         nextNode = $(tabbableNodes[nextIndex]);
899                     } while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));
900                     if (nextNode.length) {
901                         // A new f
902                         focusRelevant = nextNode.closest(stepTarget).length;
903                         focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;
904                     } else {
905                         // Unable to find the target somehow.
906                         focusRelevant = false;
907                     }
908                 }
910                 if (focusRelevant) {
911                     nextNode.focus();
912                 } else {
913                     if (e.shiftKey) {
914                         // Focus on the last tabbable node in the step.
915                         this.currentStepNode.find(tabbableSelector).last().focus();
916                     } else {
917                         if (this.currentStepConfig.isOrphan) {
918                             // Focus on the step - there is no target.
919                             this.currentStepNode.focus();
920                         } else {
921                             // Focus on the step target.
922                             stepTarget.focus();
923                         }
924                     }
925                 }
926                 e.preventDefault();
927             }).call(this);
928             break;
929     }
930 };
932 /**
933  * Start the current tour.
934  *
935  * @method  startTour
936  * @param   {Integer}   startAt     Which step number to start at. If not specified, starts at the last point.
937  * @chainable
938  */
939 Tour.prototype.startTour = function (startAt) {
940     if (typeof startAt === 'undefined') {
941         startAt = this.getCurrentStepNumber();
942     }
944     this.fireEventHandlers('beforeStart', startAt);
945     this.gotoStep(startAt);
946     this.fireEventHandlers('afterStart', startAt);
948     return this;
949 };
951 /**
952  * Restart the tour from the beginning, resetting the completionlag.
953  *
954  * @method  restartTour
955  * @chainable
956  */
957 Tour.prototype.restartTour = function () {
958     return this.startTour(0);
959 };
961 /**
962  * End the current tour.
963  *
964  * @method  endTour
965  * @chainable
966  */
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');
975             }
976             previousTarget.focus();
977         }
978     }
980     this.hide(true);
982     this.fireEventHandlers('afterEnd');
984     return this;
985 };
987 /**
988  * Hide any currently visible steps.
989  *
990  * @method hide
991  * @chainable
992  */
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();
1000         }
1001     }
1003     // Restore original target configuration.
1004     if (this.currentStepConfig) {
1005         var target = this.getStepTarget(this.currentStepConfig);
1006         if (target) {
1007             if (target.data('original-labelledby')) {
1008                 target.attr('aria-labelledby', target.data('original-labelledby'));
1009             }
1011             if (target.data('original-describedby')) {
1012                 target.attr('aria-describedby', target.data('original-describedby'));
1013             }
1015             if (target.data('original-tabindex')) {
1016                 target.attr('tabindex', target.data('tabindex'));
1017             }
1018         }
1020         // Clear the step configuration.
1021         this.currentStepConfig = null;
1022     }
1024     var fadeTime = 0;
1025     if (transition) {
1026         fadeTime = 400;
1027     }
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 () {
1033         $(this).remove();
1034     });
1036     // Reset the listeners.
1037     this.resetStepListeners();
1039     this.fireEventHandlers('afterHide');
1041     this.currentStepNode = null;
1042     this.currentStepPopper = null;
1043     return this;
1044 };
1046 /**
1047  * Show the current steps.
1048  *
1049  * @method show
1050  * @chainable
1051  */
1052 Tour.prototype.show = function () {
1053     // Show the current step.
1054     var startAt = this.getCurrentStepNumber();
1056     return this.gotoStep(startAt);
1057 };
1059 /**
1060  * Return the current step node.
1061  *
1062  * @method  getStepContainer
1063  * @return  {jQuery}
1064  */
1065 Tour.prototype.getStepContainer = function () {
1066     return $(this.currentStepNode);
1067 };
1069 /**
1070  * Calculate scrollTop.
1071  *
1072  * @method  calculateScrollTop
1073  * @param   {Object}    stepConfig      The step configuration of the step
1074  * @return  {Number}
1075  */
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;
1090     } else {
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;
1094     }
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);
1103 };
1105 /**
1106  * Calculate dialogue position for page middle.
1107  *
1108  * @method  calculateScrollTop
1109  * @return  {Number}
1110  */
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();
1120     return {
1121         top: Math.ceil(scrollTop + (viewportHeight - stepHeight) / 2),
1122         left: Math.ceil(scrollLeft + (viewportWidth - stepWidth) / 2)
1123     };
1124 };
1126 /**
1127  * Position the step on the page.
1128  *
1129  * @method  positionStep
1130  * @param   {Object}    stepConfig      The step configuration of the step
1131  * @chainable
1132  */
1133 Tour.prototype.positionStep = function (stepConfig) {
1134     var content = this.currentStepNode;
1135     if (!content || !content.length) {
1136         // Unable to find the step node.
1137         return this;
1138     }
1140     var flipBehavior = void 0;
1141     switch (stepConfig.placement) {
1142         case 'left':
1143             flipBehavior = ['left', 'right', 'top', 'bottom'];
1144             break;
1145         case 'right':
1146             flipBehavior = ['right', 'left', 'top', 'bottom'];
1147             break;
1148         case 'top':
1149             flipBehavior = ['top', 'bottom', 'right', 'left'];
1150             break;
1151         case 'bottom':
1152             flipBehavior = ['bottom', 'top', 'right', 'left'];
1153             break;
1154         default:
1155             flipBehavior = 'flip';
1156             break;
1157     }
1159     var target = this.getStepTarget(stepConfig);
1160     var background = $('[data-flexitour="step-background"]');
1161     if (background.length) {
1162         target = background;
1163     }
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']
1171     });
1173     return this;
1174 };
1176 /**
1177  * Add the backdrop.
1178  *
1179  * @method  positionBackdrop
1180  * @param   {Object}    stepConfig      The step configuration of the step
1181  * @chainable
1182  */
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);
1191             } else {
1192                 backdrop.insertAfter($(stepConfig.attachTo));
1193             }
1194         } else {
1195             $('body').append(backdrop);
1196         }
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);
1205             var buffer = 10;
1207             var colorNode = targetNode;
1208             if (buffer) {
1209                 colorNode = $('body');
1210             }
1212             background.css({
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)
1218             });
1220             if (targetNode.offset().left < buffer) {
1221                 background.css({
1222                     width: targetNode.outerWidth() + targetNode.offset().left + buffer,
1223                     left: targetNode.offset().left
1224                 });
1225             }
1227             if (targetNode.offset().top < buffer) {
1228                 background.css({
1229                     height: targetNode.outerHeight() + targetNode.offset().top + buffer,
1230                     top: targetNode.offset().top
1231                 });
1232             }
1234             var targetRadius = targetNode.css('borderRadius');
1235             if (targetRadius && targetRadius !== $('body').css('borderRadius')) {
1236                 background.css('borderRadius', targetRadius);
1237             }
1239             var targetPosition = this.calculatePosition(targetNode);
1240             if (targetPosition === 'fixed') {
1241                 background.css('top', 0);
1242             }
1244             var fader = background.clone();
1245             fader.css({
1246                 backgroundColor: backdrop.css('backgroundColor'),
1247                 opacity: backdrop.css('opacity')
1248             });
1249             fader.attr('data-flexitour', 'step-background-fader');
1251             if (stepConfig.zIndex) {
1252                 if (stepConfig.attachPoint === 'append') {
1253                     $(stepConfig.attachTo).append(background);
1254                 } else {
1255                     fader.insertAfter($(stepConfig.attachTo));
1256                     background.insertAfter($(stepConfig.attachTo));
1257                 }
1258             } else {
1259                 $('body').append(fader);
1260                 $('body').append(background);
1261             }
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);
1271             }
1273             fader.fadeOut('2000', function () {
1274                 $(this).remove();
1275             });
1276         }
1277     }
1278     return this;
1279 };
1281 /**
1282  * Calculate the inheritted z-index.
1283  *
1284  * @method  calculateZIndex
1285  * @param   {jQuery}    elem                        The element to calculate z-index for
1286  * @return  {Number}                                Calculated z-index
1287  */
1288 Tour.prototype.calculateZIndex = function (elem) {
1289     elem = $(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) {
1302                 return value;
1303             }
1304         }
1305         elem = elem.parent();
1306     }
1308     return 0;
1309 };
1311 /**
1312  * Calculate the inheritted background colour.
1313  *
1314  * @method  calculateInherittedBackgroundColor
1315  * @param   {jQuery}    elem                        The element to calculate colour for
1316  * @return  {String}                                Calculated background colour
1317  */
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');
1323     fakeNode.remove();
1325     elem = $(elem);
1326     while (elem.length && elem[0] !== document) {
1327         var color = elem.css('backgroundColor');
1328         if (color !== fakeElemColor) {
1329             return color;
1330         }
1331         elem = elem.parent();
1332     }
1334     return null;
1335 };
1337 /**
1338  * Calculate the inheritted position.
1339  *
1340  * @method  calculatePosition
1341  * @param   {jQuery}    elem                        The element to calculate position for
1342  * @return  {String}                                Calculated position
1343  */
1344 Tour.prototype.calculatePosition = function (elem) {
1345     elem = $(elem);
1346     while (elem.length && elem[0] !== document) {
1347         var position = elem.css('position');
1348         if (position !== 'static') {
1349             return position;
1350         }
1351         elem = elem.parent();
1352     }
1354     return null;
1355 };
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!');
1360         return data;
1361     }
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);
1372     return data;
1373 };
1375 if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object') {
1376     module.exports = Tour;
1379 return Tour;
1381 }));