f04bc182e59a7aa8a8b97b024970a3028b0a9489
[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), 500);
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 targetNode = this.getStepTarget(stepConfig);
727         targetNode.data('flexitour', 'target');
729         var zIndex = this.calculateZIndex(targetNode);
730         if (zIndex) {
731             stepConfig.zIndex = zIndex + 1;
732         }
734         if (stepConfig.zIndex) {
735             currentStepNode.css('zIndex', stepConfig.zIndex + 1);
736         }
738         // Add the backdrop.
739         this.positionBackdrop(stepConfig);
741         if (stepConfig.attachPoint === 'append') {
742             $(stepConfig.attachTo).append(currentStepNode);
743             this.currentStepNode = currentStepNode;
744         } else {
745             this.currentStepNode = currentStepNode.insertAfter($(stepConfig.attachTo));
746         }
748         // Ensure that the step node is positioned.
749         // Some situations mean that the value is not properly calculated without this step.
750         this.currentStepNode.css({
751             top: 0,
752             left: 0
753         });
755         animationTarget.animate({
756             scrollTop: this.calculateScrollTop(stepConfig)
757         }).promise().then(function () {
758             this.positionStep(stepConfig);
759             this.revealStep(stepConfig);
760         }.bind(this));
761     } else if (stepConfig.orphan) {
762         stepConfig.isOrphan = true;
764         // This will be appended to the body instead.
765         stepConfig.attachTo = 'body';
766         stepConfig.attachPoint = 'append';
768         // Add the backdrop.
769         this.positionBackdrop(stepConfig);
771         // This is an orphaned step.
772         currentStepNode.addClass('orphan');
774         // It lives in the body.
775         $(stepConfig.attachTo).append(currentStepNode);
776         this.currentStepNode = currentStepNode;
778         this.currentStepNode.offset(this.calculateStepPositionInPage());
779         this.currentStepNode.css('position', 'fixed');
781         this.currentStepPopper = new Popper($('body'), this.currentStepNode[0], {
782             removeOnDestroy: true,
783             placement: stepConfig.placement + '-start',
784             arrowElement: '[data-role="arrow"]',
785             // Empty the modifiers. We've already placed the step and don't want it moved.
786             modifiers: {
787                 hide: {
788                     enabled: false
789                 },
790                 applyStyle: {
791                     onLoad: null,
792                     enabled: false
793                 }
794             }
795         });
797         this.revealStep(stepConfig);
798     }
800     return this;
801 };
803 Tour.prototype.revealStep = function (stepConfig) {
804     // Fade the step in.
805     this.currentStepNode.fadeIn('', $.proxy(function () {
806         // Announce via ARIA.
807         this.announceStep(stepConfig);
809         // Focus on the current step Node.
810         this.currentStepNode.focus();
811         window.setTimeout($.proxy(function () {
812             // After a brief delay, focus again.
813             // There seems to be an issue with Jaws where it only reads the dialogue title initially.
814             // This second focus helps it to read the full dialogue.
815             if (this.currentStepNode) {
816                 this.currentStepNode.focus();
817             }
818         }, this), 100);
819     }, this));
821     return this;
822 };
824 /**
825  * Helper to announce the step on the page.
826  *
827  * @method  announceStep
828  * @param   {Object}    stepConfig      The step configuration of the step
829  * @chainable
830  */
831 Tour.prototype.announceStep = function (stepConfig) {
832     // Setup the step Dialogue as per:
833     // * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal
834     // * https://www.w3.org/TR/wai-aria-practices/#dialog_modal
836     // Generate an ID for the current step node.
837     var stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;
838     this.currentStepNode.attr('id', stepId);
840     var bodyRegion = this.currentStepNode.find('[data-placeholder="body"]').first();
841     bodyRegion.attr('id', stepId + '-body');
842     bodyRegion.attr('role', 'document');
844     var headerRegion = this.currentStepNode.find('[data-placeholder="title"]').first();
845     headerRegion.attr('id', stepId + '-title');
846     headerRegion.attr('aria-labelledby', stepId + '-body');
848     // Generally, a modal dialog has a role of dialog.
849     this.currentStepNode.attr('role', 'dialog');
850     this.currentStepNode.attr('tabindex', 0);
851     this.currentStepNode.attr('aria-labelledby', stepId + '-title');
852     this.currentStepNode.attr('aria-describedby', stepId + '-body');
854     // Configure ARIA attributes on the target.
855     var target = this.getStepTarget(stepConfig);
856     if (target) {
857         if (!target.attr('tabindex')) {
858             target.attr('tabindex', 0);
859         }
861         target.data('original-describedby', target.attr('aria-describedby')).attr('aria-describedby', stepId + '-body');
862     }
864     this.accessibilityShow(stepConfig);
866     return this;
867 };
869 /**
870  * Handle key down events.
871  *
872  * @method  handleKeyDown
873  * @param   {EventFacade} e
874  */
875 Tour.prototype.handleKeyDown = function (e) {
876     var tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], :input:enabled, [tabindex], button';
877     switch (e.keyCode) {
878         case 27:
879             this.endTour();
880             break;
882         // 9 == Tab - trap focus for items with a backdrop.
883         case 9:
884             // Tab must be handled on key up only in this instance.
885             (function () {
886                 if (!this.currentStepConfig.hasBackdrop) {
887                     // Trapping tab focus is only handled for those steps with a backdrop.
888                     return;
889                 }
891                 // Find all tabbable locations.
892                 var activeElement = $(document.activeElement);
893                 var stepTarget = this.getStepTarget(this.currentStepConfig);
894                 var tabbableNodes = $(tabbableSelector);
895                 var currentIndex = void 0;
896                 tabbableNodes.filter(function (index, element) {
897                     if (activeElement.is(element)) {
898                         currentIndex = index;
899                         return false;
900                     }
901                 });
903                 var nextIndex = void 0;
904                 var nextNode = void 0;
905                 var focusRelevant = void 0;
906                 if (currentIndex) {
907                     var direction = 1;
908                     if (e.shiftKey) {
909                         direction = -1;
910                     }
911                     nextIndex = currentIndex;
912                     do {
913                         nextIndex += direction;
914                         nextNode = $(tabbableNodes[nextIndex]);
915                     } while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));
916                     if (nextNode.length) {
917                         // A new f
918                         focusRelevant = nextNode.closest(stepTarget).length;
919                         focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;
920                     } else {
921                         // Unable to find the target somehow.
922                         focusRelevant = false;
923                     }
924                 }
926                 if (focusRelevant) {
927                     nextNode.focus();
928                 } else {
929                     if (e.shiftKey) {
930                         // Focus on the last tabbable node in the step.
931                         this.currentStepNode.find(tabbableSelector).last().focus();
932                     } else {
933                         if (this.currentStepConfig.isOrphan) {
934                             // Focus on the step - there is no target.
935                             this.currentStepNode.focus();
936                         } else {
937                             // Focus on the step target.
938                             stepTarget.focus();
939                         }
940                     }
941                 }
942                 e.preventDefault();
943             }).call(this);
944             break;
945     }
946 };
948 /**
949  * Start the current tour.
950  *
951  * @method  startTour
952  * @param   {Integer}   startAt     Which step number to start at. If not specified, starts at the last point.
953  * @chainable
954  */
955 Tour.prototype.startTour = function (startAt) {
956     if (typeof startAt === 'undefined') {
957         startAt = this.getCurrentStepNumber();
958     }
960     this.fireEventHandlers('beforeStart', startAt);
961     this.gotoStep(startAt);
962     this.fireEventHandlers('afterStart', startAt);
964     return this;
965 };
967 /**
968  * Restart the tour from the beginning, resetting the completionlag.
969  *
970  * @method  restartTour
971  * @chainable
972  */
973 Tour.prototype.restartTour = function () {
974     return this.startTour(0);
975 };
977 /**
978  * End the current tour.
979  *
980  * @method  endTour
981  * @chainable
982  */
983 Tour.prototype.endTour = function () {
984     this.fireEventHandlers('beforeEnd');
986     if (this.currentStepConfig) {
987         var previousTarget = this.getStepTarget(this.currentStepConfig);
988         if (previousTarget) {
989             if (!previousTarget.attr('tabindex')) {
990                 previousTarget.attr('tabindex', '-1');
991             }
992             previousTarget.focus();
993         }
994     }
996     this.hide(true);
998     this.fireEventHandlers('afterEnd');
1000     return this;
1001 };
1003 /**
1004  * Hide any currently visible steps.
1005  *
1006  * @method hide
1007  * @chainable
1008  */
1009 Tour.prototype.hide = function (transition) {
1010     this.fireEventHandlers('beforeHide');
1012     if (this.currentStepNode && this.currentStepNode.length) {
1013         this.currentStepNode.hide();
1014         if (this.currentStepPopper) {
1015             this.currentStepPopper.destroy();
1016         }
1017     }
1019     // Restore original target configuration.
1020     if (this.currentStepConfig) {
1021         var target = this.getStepTarget(this.currentStepConfig);
1022         if (target) {
1023             if (target.data('original-labelledby')) {
1024                 target.attr('aria-labelledby', target.data('original-labelledby'));
1025             }
1027             if (target.data('original-describedby')) {
1028                 target.attr('aria-describedby', target.data('original-describedby'));
1029             }
1031             if (target.data('original-tabindex')) {
1032                 target.attr('tabindex', target.data('tabindex'));
1033             }
1034         }
1036         // Clear the step configuration.
1037         this.currentStepConfig = null;
1038     }
1040     var fadeTime = 0;
1041     if (transition) {
1042         fadeTime = 400;
1043     }
1045     // Remove the backdrop features.
1046     $('[data-flexitour="step-background"]').remove();
1047     $('[data-flexitour="step-backdrop"]').removeAttr('data-flexitour');
1048     $('[data-flexitour="backdrop"]').fadeOut(fadeTime, function () {
1049         $(this).remove();
1050     });
1052     // Reset the listeners.
1053     this.resetStepListeners();
1055     this.accessibilityHide();
1057     this.fireEventHandlers('afterHide');
1059     this.currentStepNode = null;
1060     this.currentStepPopper = null;
1061     return this;
1062 };
1064 /**
1065  * Show the current steps.
1066  *
1067  * @method show
1068  * @chainable
1069  */
1070 Tour.prototype.show = function () {
1071     // Show the current step.
1072     var startAt = this.getCurrentStepNumber();
1074     return this.gotoStep(startAt);
1075 };
1077 /**
1078  * Return the current step node.
1079  *
1080  * @method  getStepContainer
1081  * @return  {jQuery}
1082  */
1083 Tour.prototype.getStepContainer = function () {
1084     return $(this.currentStepNode);
1085 };
1087 /**
1088  * Calculate scrollTop.
1089  *
1090  * @method  calculateScrollTop
1091  * @param   {Object}    stepConfig      The step configuration of the step
1092  * @return  {Number}
1093  */
1094 Tour.prototype.calculateScrollTop = function (stepConfig) {
1095     var scrollTop = $(window).scrollTop();
1096     var viewportHeight = $(window).height();
1097     var targetNode = this.getStepTarget(stepConfig);
1099     if (stepConfig.placement === 'top') {
1100         // If the placement is top, center scroll at the top of the target.
1101         scrollTop = targetNode.offset().top - viewportHeight / 2;
1102     } else if (stepConfig.placement === 'bottom') {
1103         // If the placement is bottom, center scroll at the bottom of the target.
1104         scrollTop = targetNode.offset().top + targetNode.height() - viewportHeight / 2;
1105     } else if (targetNode.height() <= viewportHeight * 0.8) {
1106         // If the placement is left/right, and the target fits in the viewport, centre screen on the target
1107         scrollTop = targetNode.offset().top - (viewportHeight - targetNode.height()) / 2;
1108     } else {
1109         // If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer
1110         // and change step attachmentTarget to top+.
1111         scrollTop = targetNode.offset().top - viewportHeight * 0.2;
1112     }
1114     // Never scroll over the top.
1115     scrollTop = Math.max(0, scrollTop);
1117     // Never scroll beyond the bottom.
1118     scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);
1120     return Math.ceil(scrollTop);
1121 };
1123 /**
1124  * Calculate dialogue position for page middle.
1125  *
1126  * @method  calculateScrollTop
1127  * @return  {Number}
1128  */
1129 Tour.prototype.calculateStepPositionInPage = function () {
1130     var viewportHeight = $(window).height();
1131     var stepHeight = this.currentStepNode.height();
1133     var viewportWidth = $(window).width();
1134     var stepWidth = this.currentStepNode.width();
1136     return {
1137         top: Math.ceil((viewportHeight - stepHeight) / 2),
1138         left: Math.ceil((viewportWidth - stepWidth) / 2)
1139     };
1140 };
1142 /**
1143  * Position the step on the page.
1144  *
1145  * @method  positionStep
1146  * @param   {Object}    stepConfig      The step configuration of the step
1147  * @chainable
1148  */
1149 Tour.prototype.positionStep = function (stepConfig) {
1150     var content = this.currentStepNode;
1151     if (!content || !content.length) {
1152         // Unable to find the step node.
1153         return this;
1154     }
1156     var flipBehavior = void 0;
1157     switch (stepConfig.placement) {
1158         case 'left':
1159             flipBehavior = ['left', 'right', 'top', 'bottom'];
1160             break;
1161         case 'right':
1162             flipBehavior = ['right', 'left', 'top', 'bottom'];
1163             break;
1164         case 'top':
1165             flipBehavior = ['top', 'bottom', 'right', 'left'];
1166             break;
1167         case 'bottom':
1168             flipBehavior = ['bottom', 'top', 'right', 'left'];
1169             break;
1170         default:
1171             flipBehavior = 'flip';
1172             break;
1173     }
1175     var target = this.getStepTarget(stepConfig);
1176     var config = {
1177         placement: stepConfig.placement + '-start',
1178         removeOnDestroy: true,
1179         modifiers: {
1180             flip: {
1181                 behaviour: flipBehavior
1182             },
1183             arrow: {
1184                 element: '[data-role="arrow"]'
1185             }
1186         }
1187     };
1189     var boundaryElement = target.closest('section');
1190     if (boundaryElement.length) {
1191         config.boundariesElement = boundaryElement[0];
1192     }
1194     var background = $('[data-flexitour="step-background"]');
1195     if (background.length) {
1196         target = background;
1197     }
1198     this.currentStepPopper = new Popper(target, content[0], config);
1200     return this;
1201 };
1203 /**
1204  * Add the backdrop.
1205  *
1206  * @method  positionBackdrop
1207  * @param   {Object}    stepConfig      The step configuration of the step
1208  * @chainable
1209  */
1210 Tour.prototype.positionBackdrop = function (stepConfig) {
1211     if (stepConfig.backdrop) {
1212         this.currentStepConfig.hasBackdrop = true;
1213         var backdrop = $('<div data-flexitour="backdrop"></div>');
1215         if (stepConfig.zIndex) {
1216             if (stepConfig.attachPoint === 'append') {
1217                 $(stepConfig.attachTo).append(backdrop);
1218             } else {
1219                 backdrop.insertAfter($(stepConfig.attachTo));
1220             }
1221         } else {
1222             $('body').append(backdrop);
1223         }
1225         if (this.isStepActuallyVisible(stepConfig)) {
1226             // The step has a visible target.
1227             // Punch a hole through the backdrop.
1228             var background = $('<div data-flexitour="step-background"></div>');
1230             var targetNode = this.getStepTarget(stepConfig);
1232             var buffer = 10;
1234             var colorNode = targetNode;
1235             if (buffer) {
1236                 colorNode = $('body');
1237             }
1239             background.css({
1240                 width: targetNode.outerWidth() + buffer + buffer,
1241                 height: targetNode.outerHeight() + buffer + buffer,
1242                 left: targetNode.offset().left - buffer,
1243                 top: targetNode.offset().top - buffer,
1244                 backgroundColor: this.calculateInherittedBackgroundColor(colorNode)
1245             });
1247             if (targetNode.offset().left < buffer) {
1248                 background.css({
1249                     width: targetNode.outerWidth() + targetNode.offset().left + buffer,
1250                     left: targetNode.offset().left
1251                 });
1252             }
1254             if (targetNode.offset().top < buffer) {
1255                 background.css({
1256                     height: targetNode.outerHeight() + targetNode.offset().top + buffer,
1257                     top: targetNode.offset().top
1258                 });
1259             }
1261             var targetRadius = targetNode.css('borderRadius');
1262             if (targetRadius && targetRadius !== $('body').css('borderRadius')) {
1263                 background.css('borderRadius', targetRadius);
1264             }
1266             var targetPosition = this.calculatePosition(targetNode);
1267             if (targetPosition === 'fixed') {
1268                 background.css('top', 0);
1269             }
1271             var fader = background.clone();
1272             fader.css({
1273                 backgroundColor: backdrop.css('backgroundColor'),
1274                 opacity: backdrop.css('opacity')
1275             });
1276             fader.attr('data-flexitour', 'step-background-fader');
1278             if (stepConfig.zIndex) {
1279                 if (stepConfig.attachPoint === 'append') {
1280                     $(stepConfig.attachTo).append(background);
1281                 } else {
1282                     fader.insertAfter($(stepConfig.attachTo));
1283                     background.insertAfter($(stepConfig.attachTo));
1284                 }
1285             } else {
1286                 $('body').append(fader);
1287                 $('body').append(background);
1288             }
1290             // Add the backdrop data to the actual target.
1291             // This is the part which actually does the work.
1292             targetNode.attr('data-flexitour', 'step-backdrop');
1294             if (stepConfig.zIndex) {
1295                 backdrop.css('zIndex', stepConfig.zIndex);
1296                 background.css('zIndex', stepConfig.zIndex + 1);
1297                 targetNode.css('zIndex', stepConfig.zIndex + 2);
1298             }
1300             fader.fadeOut('2000', function () {
1301                 $(this).remove();
1302             });
1303         }
1304     }
1305     return this;
1306 };
1308 /**
1309  * Calculate the inheritted z-index.
1310  *
1311  * @method  calculateZIndex
1312  * @param   {jQuery}    elem                        The element to calculate z-index for
1313  * @return  {Number}                                Calculated z-index
1314  */
1315 Tour.prototype.calculateZIndex = function (elem) {
1316     elem = $(elem);
1317     while (elem.length && elem[0] !== document) {
1318         // Ignore z-index if position is set to a value where z-index is ignored by the browser
1319         // This makes behavior of this function consistent across browsers
1320         // WebKit always returns auto if the element is positioned.
1321         var position = elem.css("position");
1322         if (position === "absolute" || position === "relative" || position === "fixed") {
1323             // IE returns 0 when zIndex is not specified
1324             // other browsers return a string
1325             // we ignore the case of nested elements with an explicit value of 0
1326             // <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
1327             var value = parseInt(elem.css("zIndex"), 10);
1328             if (!isNaN(value) && value !== 0) {
1329                 return value;
1330             }
1331         }
1332         elem = elem.parent();
1333     }
1335     return 0;
1336 };
1338 /**
1339  * Calculate the inheritted background colour.
1340  *
1341  * @method  calculateInherittedBackgroundColor
1342  * @param   {jQuery}    elem                        The element to calculate colour for
1343  * @return  {String}                                Calculated background colour
1344  */
1345 Tour.prototype.calculateInherittedBackgroundColor = function (elem) {
1346     // Use a fake node to compare each element against.
1347     var fakeNode = $('<div>').hide();
1348     $('body').append(fakeNode);
1349     var fakeElemColor = fakeNode.css('backgroundColor');
1350     fakeNode.remove();
1352     elem = $(elem);
1353     while (elem.length && elem[0] !== document) {
1354         var color = elem.css('backgroundColor');
1355         if (color !== fakeElemColor) {
1356             return color;
1357         }
1358         elem = elem.parent();
1359     }
1361     return null;
1362 };
1364 /**
1365  * Calculate the inheritted position.
1366  *
1367  * @method  calculatePosition
1368  * @param   {jQuery}    elem                        The element to calculate position for
1369  * @return  {String}                                Calculated position
1370  */
1371 Tour.prototype.calculatePosition = function (elem) {
1372     elem = $(elem);
1373     while (elem.length && elem[0] !== document) {
1374         var position = elem.css('position');
1375         if (position !== 'static') {
1376             return position;
1377         }
1378         elem = elem.parent();
1379     }
1381     return null;
1382 };
1384 /**
1385  * Perform accessibility changes for step shown.
1386  *
1387  * This will add aria-hidden="true" to all siblings and parent siblings.
1388  *
1389  * @method  accessibilityShow
1390  */
1391 Tour.prototype.accessibilityShow = function () {
1392     var stateHolder = 'data-has-hidden';
1393     var attrName = 'aria-hidden';
1394     var hideFunction = function hideFunction(child) {
1395         var flexitourRole = child.data('flexitour');
1396         if (flexitourRole) {
1397             switch (flexitourRole) {
1398                 case 'container':
1399                 case 'target':
1400                     return;
1401             }
1402         }
1404         var hidden = child.attr(attrName);
1405         if (!hidden) {
1406             child.attr(stateHolder, true);
1407             child.attr(attrName, true);
1408         }
1409     };
1411     this.currentStepNode.siblings().each(function (index, node) {
1412         hideFunction($(node));
1413     });
1414     this.currentStepNode.parentsUntil('body').siblings().each(function (index, node) {
1415         hideFunction($(node));
1416     });
1417 };
1419 /**
1420  * Perform accessibility changes for step hidden.
1421  *
1422  * This will remove any newly added aria-hidden="true".
1423  *
1424  * @method  accessibilityHide
1425  */
1426 Tour.prototype.accessibilityHide = function () {
1427     var stateHolder = 'data-has-hidden';
1428     var attrName = 'aria-hidden';
1429     var showFunction = function showFunction(child) {
1430         var hidden = child.attr(stateHolder);
1431         if (typeof hidden !== 'undefined') {
1432             child.removeAttr(stateHolder);
1433             child.removeAttr(attrName);
1434         }
1435     };
1437     $('[' + stateHolder + ']').each(function (index, node) {
1438         showFunction($(node));
1439     });
1440 };
1442 if ((typeof exports === 'undefined' ? 'undefined' : _typeof(exports)) === 'object') {
1443     module.exports = Tour;
1446 return Tour;
1448 }));