懒羊羊
2023-08-30 1ac2bc1590406d9babec036e154d8d08f34a6aa1
提交 | 用户 | 时间
1ac2bc 1 /**
2  * Intro.js v2.9.3
3  * https://github.com/usablica/intro.js
4  *
5  * Copyright (C) 2017 Afshin Mehrabani (@afshinmeh)
6  */
7
8 (function (f) {
9     if (typeof exports === "object" && typeof module !== "undefined") {
10         module.exports = f();
11         // deprecated function
12         // @since 2.8.0
13         module.exports.introJs = function () {
14             console.warn('Deprecated: please use require("introJs.js") directly, instead of the introJs method of the function');
15             // introJs()
16             return f().apply(this, arguments);
17         };
18     } else if (typeof define === "function" && define.amd) {
19         define([], f);
20     } else if (window.layui && layui.define) {  // layui加载
21         layui.define(function (exports) {
22             layui.link(layui.cache.base + 'introJs/introJs.css');
23             exports('introJs', f());
24         });
25     } else {
26         var g;
27         if (typeof window !== "undefined") {
28             g = window;
29         } else if (typeof global !== "undefined") {
30             g = global;
31         } else if (typeof self !== "undefined") {
32             g = self;
33         } else {
34             g = this;
35         }
36         g.introJs = f();
37     }
38 })(function () {
39     //Default config/variables
40     var VERSION = '2.9.3';
41
42     /**
43      * IntroJs main class
44      *
45      * @class IntroJs
46      */
47     function IntroJs(obj) {
48         this._targetElement = obj;
49         this._introItems = [];
50
51         this._options = {
52             /* Next button label in tooltip box */
53             nextLabel: '下一步 →',
54             /* Previous button label in tooltip box */
55             prevLabel: '← 上一步',
56             /* Skip button label in tooltip box */
57             skipLabel: '跳过',
58             /* Done button label in tooltip box */
59             doneLabel: '完成',
60             /* Hide previous button in the first step? Otherwise, it will be disabled button. */
61             hidePrev: false,
62             /* Hide next button in the last step? Otherwise, it will be disabled button. */
63             hideNext: false,
64             /* Default tooltip box position */
65             tooltipPosition: 'bottom',
66             /* Next CSS class for tooltip boxes */
67             tooltipClass: '',
68             /* CSS class that is added to the helperLayer */
69             highlightClass: '',
70             /* Close introduction when pressing Escape button? */
71             exitOnEsc: true,
72             /* Close introduction when clicking on overlay layer? */
73             exitOnOverlayClick: true,
74             /* Show step numbers in introduction? */
75             showStepNumbers: true,
76             /* Let user use keyboard to navigate the tour? */
77             keyboardNavigation: true,
78             /* Show tour control buttons? */
79             showButtons: true,
80             /* Show tour bullets? */
81             showBullets: true,
82             /* Show tour progress? */
83             showProgress: false,
84             /* Scroll to highlighted element? */
85             scrollToElement: true,
86             /*
87              * Should we scroll the tooltip or target element?
88              *
89              * Options are: 'element' or 'tooltip'
90              */
91             scrollTo: 'element',
92             /* Padding to add after scrolling when element is not in the viewport (in pixels) */
93             scrollPadding: 30,
94             /* Set the overlay opacity */
95             overlayOpacity: 0.2,
96             /* Precedence of positions, when auto is enabled */
97             positionPrecedence: ["bottom", "top", "right", "left"],
98             /* Disable an interaction with element? */
99             disableInteraction: false,
100             /* Set how much padding to be used around helper element */
101             helperElementPadding: 10,
102             /* Default hint position */
103             hintPosition: 'top-middle',
104             /* Hint button label */
105             hintButtonLabel: 'Got it',
106             /* Adding animation to hints? */
107             hintAnimation: true,
108             /* additional classes to put on the buttons */
109             buttonClass: "introjs-button"
110         };
111     }
112
113     /**
114      * Initiate a new introduction/guide from an element in the page
115      *
116      * @api private
117      * @method _introForElement
118      * @param {Object} targetElm
119      * @param {String} group
120      * @returns {Boolean} Success or not?
121      */
122     function _introForElement(targetElm, group) {
123         var allIntroSteps = targetElm.querySelectorAll("*[data-intro]"),
124             introItems = [];
125
126         if (this._options.steps) {
127             //use steps passed programmatically
128             _forEach(this._options.steps, function (step) {
129                 var currentItem = _cloneObject(step);
130
131                 //set the step
132                 currentItem.step = introItems.length + 1;
133
134                 //use querySelector function only when developer used CSS selector
135                 if (typeof (currentItem.element) === 'string') {
136                     //grab the element with given selector from the page
137                     currentItem.element = document.querySelector(currentItem.element);
138                 }
139
140                 //intro without element
141                 if (typeof (currentItem.element) === 'undefined' || currentItem.element === null) {
142                     var floatingElementQuery = document.querySelector(".introjsFloatingElement");
143
144                     if (floatingElementQuery === null) {
145                         floatingElementQuery = document.createElement('div');
146                         floatingElementQuery.className = 'introjsFloatingElement';
147
148                         document.body.appendChild(floatingElementQuery);
149                     }
150
151                     currentItem.element = floatingElementQuery;
152                     currentItem.position = 'floating';
153                 }
154
155                 currentItem.scrollTo = currentItem.scrollTo || this._options.scrollTo;
156
157                 if (typeof (currentItem.disableInteraction) === 'undefined') {
158                     currentItem.disableInteraction = this._options.disableInteraction;
159                 }
160
161                 if (currentItem.element !== null) {
162                     introItems.push(currentItem);
163                 }
164             }.bind(this));
165
166         } else {
167             //use steps from data-* annotations
168             var elmsLength = allIntroSteps.length;
169             var disableInteraction;
170
171             //if there's no element to intro
172             if (elmsLength < 1) {
173                 return false;
174             }
175
176             _forEach(allIntroSteps, function (currentElement) {
177
178                 // PR #80
179                 // start intro for groups of elements
180                 if (group && (currentElement.getAttribute("data-intro-group") !== group)) {
181                     return;
182                 }
183
184                 // skip hidden elements
185                 if (currentElement.style.display === 'none') {
186                     return;
187                 }
188
189                 var step = parseInt(currentElement.getAttribute('data-step'), 10);
190
191                 if (typeof (currentElement.getAttribute('data-disable-interaction')) !== 'undefined') {
192                     disableInteraction = !!currentElement.getAttribute('data-disable-interaction');
193                 } else {
194                     disableInteraction = this._options.disableInteraction;
195                 }
196
197                 if (step > 0) {
198                     introItems[step - 1] = {
199                         element: currentElement,
200                         intro: currentElement.getAttribute('data-intro'),
201                         step: parseInt(currentElement.getAttribute('data-step'), 10),
202                         tooltipClass: currentElement.getAttribute('data-tooltipclass'),
203                         highlightClass: currentElement.getAttribute('data-highlightclass'),
204                         position: currentElement.getAttribute('data-position') || this._options.tooltipPosition,
205                         scrollTo: currentElement.getAttribute('data-scrollto') || this._options.scrollTo,
206                         disableInteraction: disableInteraction
207                     };
208                 }
209             }.bind(this));
210
211             //next add intro items without data-step
212             //todo: we need a cleanup here, two loops are redundant
213             var nextStep = 0;
214
215             _forEach(allIntroSteps, function (currentElement) {
216
217                 // PR #80
218                 // start intro for groups of elements
219                 if (group && (currentElement.getAttribute("data-intro-group") !== group)) {
220                     return;
221                 }
222
223                 if (currentElement.getAttribute('data-step') === null) {
224
225                     while (true) {
226                         if (typeof introItems[nextStep] === 'undefined') {
227                             break;
228                         } else {
229                             nextStep++;
230                         }
231                     }
232
233                     if (typeof (currentElement.getAttribute('data-disable-interaction')) !== 'undefined') {
234                         disableInteraction = !!currentElement.getAttribute('data-disable-interaction');
235                     } else {
236                         disableInteraction = this._options.disableInteraction;
237                     }
238
239                     introItems[nextStep] = {
240                         element: currentElement,
241                         intro: currentElement.getAttribute('data-intro'),
242                         step: nextStep + 1,
243                         tooltipClass: currentElement.getAttribute('data-tooltipclass'),
244                         highlightClass: currentElement.getAttribute('data-highlightclass'),
245                         position: currentElement.getAttribute('data-position') || this._options.tooltipPosition,
246                         scrollTo: currentElement.getAttribute('data-scrollto') || this._options.scrollTo,
247                         disableInteraction: disableInteraction
248                     };
249                 }
250             }.bind(this));
251         }
252
253         //removing undefined/null elements
254         var tempIntroItems = [];
255         for (var z = 0; z < introItems.length; z++) {
256             if (introItems[z]) {
257                 // copy non-falsy values to the end of the array
258                 tempIntroItems.push(introItems[z]);
259             }
260         }
261
262         introItems = tempIntroItems;
263
264         //Ok, sort all items with given steps
265         introItems.sort(function (a, b) {
266             return a.step - b.step;
267         });
268
269         //set it to the introJs object
270         this._introItems = introItems;
271
272         //add overlay layer to the page
273         if (_addOverlayLayer.call(this, targetElm)) {
274             //then, start the show
275             _nextStep.call(this);
276
277             if (this._options.keyboardNavigation) {
278                 DOMEvent.on(window, 'keydown', _onKeyDown, this, true);
279             }
280             //for window resize
281             DOMEvent.on(window, 'resize', _onResize, this, true);
282         }
283         return false;
284     }
285
286     function _onResize() {
287         this.refresh.call(this);
288     }
289
290     /**
291      * on keyCode:
292      * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
293      * This feature has been removed from the Web standards.
294      * Though some browsers may still support it, it is in
295      * the process of being dropped.
296      * Instead, you should use KeyboardEvent.code,
297      * if it's implemented.
298      *
299      * jQuery's approach is to test for
300      *   (1) e.which, then
301      *   (2) e.charCode, then
302      *   (3) e.keyCode
303      * https://github.com/jquery/jquery/blob/a6b0705294d336ae2f63f7276de0da1195495363/src/event.js#L638
304      *
305      * @param type var
306      * @return type
307      */
308     function _onKeyDown(e) {
309         var code = (e.code === null) ? e.which : e.code;
310
311         // if code/e.which is null
312         if (code === null) {
313             code = (e.charCode === null) ? e.keyCode : e.charCode;
314         }
315
316         if ((code === 'Escape' || code === 27) && this._options.exitOnEsc === true) {
317             //escape key pressed, exit the intro
318             //check if exit callback is defined
319             _exitIntro.call(this, this._targetElement);
320         } else if (code === 'ArrowLeft' || code === 37) {
321             //left arrow
322             _previousStep.call(this);
323         } else if (code === 'ArrowRight' || code === 39) {
324             //right arrow
325             _nextStep.call(this);
326         } else if (code === 'Enter' || code === 13) {
327             //srcElement === ie
328             var target = e.target || e.srcElement;
329             if (target && target.className.match('introjs-prevbutton')) {
330                 //user hit enter while focusing on previous button
331                 _previousStep.call(this);
332             } else if (target && target.className.match('introjs-skipbutton')) {
333                 //user hit enter while focusing on skip button
334                 if (this._introItems.length - 1 === this._currentStep && typeof (this._introCompleteCallback) === 'function') {
335                     this._introCompleteCallback.call(this);
336                 }
337
338                 _exitIntro.call(this, this._targetElement);
339             } else if (target && target.getAttribute('data-stepnumber')) {
340                 // user hit enter while focusing on step bullet
341                 target.click();
342             } else {
343                 //default behavior for responding to enter
344                 _nextStep.call(this);
345             }
346
347             //prevent default behaviour on hitting Enter, to prevent steps being skipped in some browsers
348             if (e.preventDefault) {
349                 e.preventDefault();
350             } else {
351                 e.returnValue = false;
352             }
353         }
354     }
355
356     /*
357       * makes a copy of the object
358       * @api private
359       * @method _cloneObject
360      */
361     function _cloneObject(object) {
362         if (object === null || typeof (object) !== 'object' || typeof (object.nodeType) !== 'undefined') {
363             return object;
364         }
365         var temp = {};
366         for (var key in object) {
367             if (typeof (window.jQuery) !== 'undefined' && object[key] instanceof window.jQuery) {
368                 temp[key] = object[key];
369             } else {
370                 temp[key] = _cloneObject(object[key]);
371             }
372         }
373         return temp;
374     }
375
376     /**
377      * Go to specific step of introduction
378      *
379      * @api private
380      * @method _goToStep
381      */
382     function _goToStep(step) {
383         //because steps starts with zero
384         this._currentStep = step - 2;
385         if (typeof (this._introItems) !== 'undefined') {
386             _nextStep.call(this);
387         }
388     }
389
390     /**
391      * Go to the specific step of introduction with the explicit [data-step] number
392      *
393      * @api private
394      * @method _goToStepNumber
395      */
396     function _goToStepNumber(step) {
397         this._currentStepNumber = step;
398         if (typeof (this._introItems) !== 'undefined') {
399             _nextStep.call(this);
400         }
401     }
402
403     /**
404      * Go to next step on intro
405      *
406      * @api private
407      * @method _nextStep
408      */
409     function _nextStep() {
410         this._direction = 'forward';
411
412         if (typeof (this._currentStepNumber) !== 'undefined') {
413             _forEach(this._introItems, function (item, i) {
414                 if (item.step === this._currentStepNumber) {
415                     this._currentStep = i - 1;
416                     this._currentStepNumber = undefined;
417                 }
418             }.bind(this));
419         }
420
421         if (typeof (this._currentStep) === 'undefined') {
422             this._currentStep = 0;
423         } else {
424             ++this._currentStep;
425         }
426
427         var nextStep = this._introItems[this._currentStep];
428         var continueStep = true;
429
430         if (typeof (this._introBeforeChangeCallback) !== 'undefined') {
431             continueStep = this._introBeforeChangeCallback.call(this, nextStep.element);
432         }
433
434         // if `onbeforechange` returned `false`, stop displaying the element
435         if (continueStep === false) {
436             --this._currentStep;
437             return false;
438         }
439
440         if ((this._introItems.length) <= this._currentStep) {
441             //end of the intro
442             //check if any callback is defined
443             if (typeof (this._introCompleteCallback) === 'function') {
444                 this._introCompleteCallback.call(this);
445             }
446             _exitIntro.call(this, this._targetElement);
447             return;
448         }
449
450         _showElement.call(this, nextStep);
451     }
452
453     /**
454      * Go to previous step on intro
455      *
456      * @api private
457      * @method _previousStep
458      */
459     function _previousStep() {
460         this._direction = 'backward';
461
462         if (this._currentStep === 0) {
463             return false;
464         }
465
466         --this._currentStep;
467
468         var nextStep = this._introItems[this._currentStep];
469         var continueStep = true;
470
471         if (typeof (this._introBeforeChangeCallback) !== 'undefined') {
472             continueStep = this._introBeforeChangeCallback.call(this, nextStep.element);
473         }
474
475         // if `onbeforechange` returned `false`, stop displaying the element
476         if (continueStep === false) {
477             ++this._currentStep;
478             return false;
479         }
480
481         _showElement.call(this, nextStep);
482     }
483
484     /**
485      * Update placement of the intro objects on the screen
486      * @api private
487      */
488     function _refresh() {
489         // re-align intros
490         _setHelperLayerPosition.call(this, document.querySelector('.introjs-helperLayer'));
491         _setHelperLayerPosition.call(this, document.querySelector('.introjs-tooltipReferenceLayer'));
492         _setHelperLayerPosition.call(this, document.querySelector('.introjs-disableInteraction'));
493
494         // re-align tooltip
495         if (this._currentStep !== undefined && this._currentStep !== null) {
496             var oldHelperNumberLayer = document.querySelector('.introjs-helperNumberLayer'),
497                 oldArrowLayer = document.querySelector('.introjs-arrow'),
498                 oldtooltipContainer = document.querySelector('.introjs-tooltip');
499             _placeTooltip.call(this, this._introItems[this._currentStep].element, oldtooltipContainer, oldArrowLayer, oldHelperNumberLayer);
500         }
501
502         //re-align hints
503         _reAlignHints.call(this);
504         return this;
505     }
506
507     /**
508      * Exit from intro
509      *
510      * @api private
511      * @method _exitIntro
512      * @param {Object} targetElement
513      * @param {Boolean} force - Setting to `true` will skip the result of beforeExit callback
514      */
515     function _exitIntro(targetElement, force) {
516         var continueExit = true;
517
518         // calling onbeforeexit callback
519         //
520         // If this callback return `false`, it would halt the process
521         if (this._introBeforeExitCallback !== undefined) {
522             continueExit = this._introBeforeExitCallback.call(this);
523         }
524
525         // skip this check if `force` parameter is `true`
526         // otherwise, if `onbeforeexit` returned `false`, don't exit the intro
527         if (!force && continueExit === false) return;
528
529         //remove overlay layers from the page
530         var overlayLayers = targetElement.querySelectorAll('.introjs-overlay');
531
532         if (overlayLayers && overlayLayers.length) {
533             _forEach(overlayLayers, function (overlayLayer) {
534                 overlayLayer.style.opacity = 0;
535                 window.setTimeout(function () {
536                     if (this.parentNode) {
537                         this.parentNode.removeChild(this);
538                     }
539                 }.bind(overlayLayer), 500);
540             }.bind(this));
541         }
542
543         //remove all helper layers
544         var helperLayer = targetElement.querySelector('.introjs-helperLayer');
545         if (helperLayer) {
546             helperLayer.parentNode.removeChild(helperLayer);
547         }
548
549         var referenceLayer = targetElement.querySelector('.introjs-tooltipReferenceLayer');
550         if (referenceLayer) {
551             referenceLayer.parentNode.removeChild(referenceLayer);
552         }
553
554         //remove disableInteractionLayer
555         var disableInteractionLayer = targetElement.querySelector('.introjs-disableInteraction');
556         if (disableInteractionLayer) {
557             disableInteractionLayer.parentNode.removeChild(disableInteractionLayer);
558         }
559
560         //remove intro floating element
561         var floatingElement = document.querySelector('.introjsFloatingElement');
562         if (floatingElement) {
563             floatingElement.parentNode.removeChild(floatingElement);
564         }
565
566         _removeShowElement();
567
568         //remove `introjs-fixParent` class from the elements
569         var fixParents = document.querySelectorAll('.introjs-fixParent');
570         _forEach(fixParents, function (parent) {
571             _removeClass(parent, /introjs-fixParent/g);
572         });
573
574         //clean listeners
575         DOMEvent.off(window, 'keydown', _onKeyDown, this, true);
576         DOMEvent.off(window, 'resize', _onResize, this, true);
577
578         //check if any callback is defined
579         if (this._introExitCallback !== undefined) {
580             this._introExitCallback.call(this);
581         }
582
583         //set the step to zero
584         this._currentStep = undefined;
585     }
586
587     /**
588      * Render tooltip box in the page
589      *
590      * @api private
591      * @method _placeTooltip
592      * @param {HTMLElement} targetElement
593      * @param {HTMLElement} tooltipLayer
594      * @param {HTMLElement} arrowLayer
595      * @param {HTMLElement} helperNumberLayer
596      * @param {Boolean} hintMode
597      */
598     function _placeTooltip(targetElement, tooltipLayer, arrowLayer, helperNumberLayer, hintMode) {
599         var tooltipCssClass = '',
600             currentStepObj,
601             tooltipOffset,
602             targetOffset,
603             windowSize,
604             currentTooltipPosition;
605
606         hintMode = hintMode || false;
607
608         //reset the old style
609         tooltipLayer.style.top = null;
610         tooltipLayer.style.right = null;
611         tooltipLayer.style.bottom = null;
612         tooltipLayer.style.left = null;
613         tooltipLayer.style.marginLeft = null;
614         tooltipLayer.style.marginTop = null;
615
616         arrowLayer.style.display = 'inherit';
617
618         if (typeof (helperNumberLayer) !== 'undefined' && helperNumberLayer !== null) {
619             helperNumberLayer.style.top = null;
620             helperNumberLayer.style.left = null;
621         }
622
623         //prevent error when `this._currentStep` is undefined
624         if (!this._introItems[this._currentStep]) return;
625
626         //if we have a custom css class for each step
627         currentStepObj = this._introItems[this._currentStep];
628         if (typeof (currentStepObj.tooltipClass) === 'string') {
629             tooltipCssClass = currentStepObj.tooltipClass;
630         } else {
631             tooltipCssClass = this._options.tooltipClass;
632         }
633
634         tooltipLayer.className = ('introjs-tooltip ' + tooltipCssClass).replace(/^\s+|\s+$/g, '');
635         tooltipLayer.setAttribute('role', 'dialog');
636
637         currentTooltipPosition = this._introItems[this._currentStep].position;
638
639         // Floating is always valid, no point in calculating
640         if (currentTooltipPosition !== "floating") {
641             currentTooltipPosition = _determineAutoPosition.call(this, targetElement, tooltipLayer, currentTooltipPosition);
642         }
643
644         var tooltipLayerStyleLeft;
645         targetOffset = _getOffset(targetElement);
646         tooltipOffset = _getOffset(tooltipLayer);
647         windowSize = _getWinSize();
648
649         _addClass(tooltipLayer, 'introjs-' + currentTooltipPosition);
650
651         switch (currentTooltipPosition) {
652             case 'top-right-aligned':
653                 arrowLayer.className = 'introjs-arrow bottom-right';
654
655                 var tooltipLayerStyleRight = 0;
656                 _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer);
657                 tooltipLayer.style.bottom = (targetOffset.height + 20) + 'px';
658                 break;
659
660             case 'top-middle-aligned':
661                 arrowLayer.className = 'introjs-arrow bottom-middle';
662
663                 var tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2;
664
665                 // a fix for middle aligned hints
666                 if (hintMode) {
667                     tooltipLayerStyleLeftRight += 5;
668                 }
669
670                 if (_checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) {
671                     tooltipLayer.style.right = null;
672                     _checkRight(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, windowSize, tooltipLayer);
673                 }
674                 tooltipLayer.style.bottom = (targetOffset.height + 20) + 'px';
675                 break;
676
677             case 'top-left-aligned':
678             // top-left-aligned is the same as the default top
679             case 'top':
680                 arrowLayer.className = 'introjs-arrow bottom';
681
682                 tooltipLayerStyleLeft = (hintMode) ? 0 : 15;
683
684                 _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer);
685                 tooltipLayer.style.bottom = (targetOffset.height + 20) + 'px';
686                 break;
687             case 'right':
688                 tooltipLayer.style.left = (targetOffset.width + 20) + 'px';
689                 if (targetOffset.top + tooltipOffset.height > windowSize.height) {
690                     // In this case, right would have fallen below the bottom of the screen.
691                     // Modify so that the bottom of the tooltip connects with the target
692                     arrowLayer.className = "introjs-arrow left-bottom";
693                     tooltipLayer.style.top = "-" + (tooltipOffset.height - targetOffset.height - 20) + "px";
694                 } else {
695                     arrowLayer.className = 'introjs-arrow left';
696                 }
697                 break;
698             case 'left':
699                 if (!hintMode && this._options.showStepNumbers === true) {
700                     tooltipLayer.style.top = '15px';
701                 }
702
703                 if (targetOffset.top + tooltipOffset.height > windowSize.height) {
704                     // In this case, left would have fallen below the bottom of the screen.
705                     // Modify so that the bottom of the tooltip connects with the target
706                     tooltipLayer.style.top = "-" + (tooltipOffset.height - targetOffset.height - 20) + "px";
707                     arrowLayer.className = 'introjs-arrow right-bottom';
708                 } else {
709                     arrowLayer.className = 'introjs-arrow right';
710                 }
711                 tooltipLayer.style.right = (targetOffset.width + 20) + 'px';
712
713                 break;
714             case 'floating':
715                 arrowLayer.style.display = 'none';
716
717                 //we have to adjust the top and left of layer manually for intro items without element
718                 tooltipLayer.style.left = '50%';
719                 tooltipLayer.style.top = '50%';
720                 tooltipLayer.style.marginLeft = '-' + (tooltipOffset.width / 2) + 'px';
721                 tooltipLayer.style.marginTop = '-' + (tooltipOffset.height / 2) + 'px';
722
723                 if (typeof (helperNumberLayer) !== 'undefined' && helperNumberLayer !== null) {
724                     helperNumberLayer.style.left = '-' + ((tooltipOffset.width / 2) + 18) + 'px';
725                     helperNumberLayer.style.top = '-' + ((tooltipOffset.height / 2) + 18) + 'px';
726                 }
727
728                 break;
729             case 'bottom-right-aligned':
730                 arrowLayer.className = 'introjs-arrow top-right';
731
732                 tooltipLayerStyleRight = 0;
733                 _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer);
734                 tooltipLayer.style.top = (targetOffset.height + 20) + 'px';
735                 break;
736
737             case 'bottom-middle-aligned':
738                 arrowLayer.className = 'introjs-arrow top-middle';
739
740                 tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2;
741
742                 // a fix for middle aligned hints
743                 if (hintMode) {
744                     tooltipLayerStyleLeftRight += 5;
745                 }
746
747                 if (_checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) {
748                     tooltipLayer.style.right = null;
749                     _checkRight(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, windowSize, tooltipLayer);
750                 }
751                 tooltipLayer.style.top = (targetOffset.height + 20) + 'px';
752                 break;
753
754             // case 'bottom-left-aligned':
755             // Bottom-left-aligned is the same as the default bottom
756             // case 'bottom':
757             // Bottom going to follow the default behavior
758             default:
759                 arrowLayer.className = 'introjs-arrow top';
760
761                 tooltipLayerStyleLeft = 0;
762                 _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer);
763                 tooltipLayer.style.top = (targetOffset.height + 20) + 'px';
764         }
765     }
766
767     /**
768      * Set tooltip left so it doesn't go off the right side of the window
769      *
770      * @return boolean true, if tooltipLayerStyleLeft is ok.  false, otherwise.
771      */
772     function _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer) {
773         if (targetOffset.left + tooltipLayerStyleLeft + tooltipOffset.width > windowSize.width) {
774             // off the right side of the window
775             tooltipLayer.style.left = (windowSize.width - tooltipOffset.width - targetOffset.left) + 'px';
776             return false;
777         }
778         tooltipLayer.style.left = tooltipLayerStyleLeft + 'px';
779         return true;
780     }
781
782     /**
783      * Set tooltip right so it doesn't go off the left side of the window
784      *
785      * @return boolean true, if tooltipLayerStyleRight is ok.  false, otherwise.
786      */
787     function _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer) {
788         if (targetOffset.left + targetOffset.width - tooltipLayerStyleRight - tooltipOffset.width < 0) {
789             // off the left side of the window
790             tooltipLayer.style.left = (-targetOffset.left) + 'px';
791             return false;
792         }
793         tooltipLayer.style.right = tooltipLayerStyleRight + 'px';
794         return true;
795     }
796
797     /**
798      * Determines the position of the tooltip based on the position precedence and availability
799      * of screen space.
800      *
801      * @param {Object}    targetElement
802      * @param {Object}    tooltipLayer
803      * @param {String}    desiredTooltipPosition
804      * @return {String}   calculatedPosition
805      */
806     function _determineAutoPosition(targetElement, tooltipLayer, desiredTooltipPosition) {
807
808         // Take a clone of position precedence. These will be the available
809         var possiblePositions = this._options.positionPrecedence.slice();
810
811         var windowSize = _getWinSize();
812         var tooltipHeight = _getOffset(tooltipLayer).height + 10;
813         var tooltipWidth = _getOffset(tooltipLayer).width + 20;
814         var targetElementRect = targetElement.getBoundingClientRect();
815
816         // If we check all the possible areas, and there are no valid places for the tooltip, the element
817         // must take up most of the screen real estate. Show the tooltip floating in the middle of the screen.
818         var calculatedPosition = "floating";
819
820         /*
821         * auto determine position
822         */
823
824         // Check for space below
825         if (targetElementRect.bottom + tooltipHeight + tooltipHeight > windowSize.height) {
826             _removeEntry(possiblePositions, "bottom");
827         }
828
829         // Check for space above
830         if (targetElementRect.top - tooltipHeight < 0) {
831             _removeEntry(possiblePositions, "top");
832         }
833
834         // Check for space to the right
835         if (targetElementRect.right + tooltipWidth > windowSize.width) {
836             _removeEntry(possiblePositions, "right");
837         }
838
839         // Check for space to the left
840         if (targetElementRect.left - tooltipWidth < 0) {
841             _removeEntry(possiblePositions, "left");
842         }
843
844         // @var {String}  ex: 'right-aligned'
845         var desiredAlignment = (function (pos) {
846             var hyphenIndex = pos.indexOf('-');
847             if (hyphenIndex !== -1) {
848                 // has alignment
849                 return pos.substr(hyphenIndex);
850             }
851             return '';
852         })(desiredTooltipPosition || '');
853
854         // strip alignment from position
855         if (desiredTooltipPosition) {
856             // ex: "bottom-right-aligned"
857             // should return 'bottom'
858             desiredTooltipPosition = desiredTooltipPosition.split('-')[0];
859         }
860
861         if (possiblePositions.length) {
862             if (desiredTooltipPosition !== "auto" &&
863                 possiblePositions.indexOf(desiredTooltipPosition) > -1) {
864                 // If the requested position is in the list, choose that
865                 calculatedPosition = desiredTooltipPosition;
866             } else {
867                 // Pick the first valid position, in order
868                 calculatedPosition = possiblePositions[0];
869             }
870         }
871
872         // only top and bottom positions have optional alignments
873         if (['top', 'bottom'].indexOf(calculatedPosition) !== -1) {
874             calculatedPosition += _determineAutoAlignment(targetElementRect.left, tooltipWidth, windowSize, desiredAlignment);
875         }
876
877         return calculatedPosition;
878     }
879
880     /**
881      * auto-determine alignment
882      * @param {Integer}  offsetLeft
883      * @param {Integer}  tooltipWidth
884      * @param {Object}   windowSize
885      * @param {String}   desiredAlignment
886      * @return {String}  calculatedAlignment
887      */
888     function _determineAutoAlignment(offsetLeft, tooltipWidth, windowSize, desiredAlignment) {
889         var halfTooltipWidth = tooltipWidth / 2,
890             winWidth = Math.min(windowSize.width, window.screen.width),
891             possibleAlignments = ['-left-aligned', '-middle-aligned', '-right-aligned'],
892             calculatedAlignment = '';
893
894         // valid left must be at least a tooltipWidth
895         // away from right side
896         if (winWidth - offsetLeft < tooltipWidth) {
897             _removeEntry(possibleAlignments, '-left-aligned');
898         }
899
900         // valid middle must be at least half
901         // width away from both sides
902         if (offsetLeft < halfTooltipWidth ||
903             winWidth - offsetLeft < halfTooltipWidth) {
904             _removeEntry(possibleAlignments, '-middle-aligned');
905         }
906
907         // valid right must be at least a tooltipWidth
908         // width away from left side
909         if (offsetLeft < tooltipWidth) {
910             _removeEntry(possibleAlignments, '-right-aligned');
911         }
912
913         if (possibleAlignments.length) {
914             if (possibleAlignments.indexOf(desiredAlignment) !== -1) {
915                 // the desired alignment is valid
916                 calculatedAlignment = desiredAlignment;
917             } else {
918                 // pick the first valid position, in order
919                 calculatedAlignment = possibleAlignments[0];
920             }
921         } else {
922             // if screen width is too small
923             // for ANY alignment, middle is
924             // probably the best for visibility
925             calculatedAlignment = '-middle-aligned';
926         }
927
928         return calculatedAlignment;
929     }
930
931     /**
932      * Remove an entry from a string array if it's there, does nothing if it isn't there.
933      *
934      * @param {Array} stringArray
935      * @param {String} stringToRemove
936      */
937     function _removeEntry(stringArray, stringToRemove) {
938         if (stringArray.indexOf(stringToRemove) > -1) {
939             stringArray.splice(stringArray.indexOf(stringToRemove), 1);
940         }
941     }
942
943     /**
944      * Update the position of the helper layer on the screen
945      *
946      * @api private
947      * @method _setHelperLayerPosition
948      * @param {Object} helperLayer
949      */
950     function _setHelperLayerPosition(helperLayer) {
951         if (helperLayer) {
952             //prevent error when `this._currentStep` in undefined
953             if (!this._introItems[this._currentStep]) return;
954
955             var currentElement = this._introItems[this._currentStep],
956                 elementPosition = _getOffset(currentElement.element),
957                 widthHeightPadding = this._options.helperElementPadding;
958
959             // If the target element is fixed, the tooltip should be fixed as well.
960             // Otherwise, remove a fixed class that may be left over from the previous
961             // step.
962             if (_isFixed(currentElement.element)) {
963                 _addClass(helperLayer, 'introjs-fixedTooltip');
964             } else {
965                 _removeClass(helperLayer, 'introjs-fixedTooltip');
966             }
967
968             if (currentElement.position === 'floating') {
969                 widthHeightPadding = 0;
970             }
971
972             //set new position to helper layer
973             helperLayer.style.cssText = 'width: ' + (elementPosition.width + widthHeightPadding) + 'px; ' +
974                 'height:' + (elementPosition.height + widthHeightPadding) + 'px; ' +
975                 'top:' + (elementPosition.top - widthHeightPadding / 2) + 'px;' +
976                 'left: ' + (elementPosition.left - widthHeightPadding / 2) + 'px;';
977
978         }
979     }
980
981     /**
982      * Add disableinteraction layer and adjust the size and position of the layer
983      *
984      * @api private
985      * @method _disableInteraction
986      */
987     function _disableInteraction() {
988         var disableInteractionLayer = document.querySelector('.introjs-disableInteraction');
989
990         if (disableInteractionLayer === null) {
991             disableInteractionLayer = document.createElement('div');
992             disableInteractionLayer.className = 'introjs-disableInteraction';
993             this._targetElement.appendChild(disableInteractionLayer);
994         }
995
996         _setHelperLayerPosition.call(this, disableInteractionLayer);
997     }
998
999     /**
1000      * Setting anchors to behave like buttons
1001      *
1002      * @api private
1003      * @method _setAnchorAsButton
1004      */
1005     function _setAnchorAsButton(anchor) {
1006         anchor.setAttribute('role', 'button');
1007         anchor.tabIndex = 0;
1008     }
1009
1010     /**
1011      * Show an element on the page
1012      *
1013      * @api private
1014      * @method _showElement
1015      * @param {Object} targetElement
1016      */
1017     function _showElement(targetElement) {
1018         if (typeof (this._introChangeCallback) !== 'undefined') {
1019             this._introChangeCallback.call(this, targetElement.element);
1020         }
1021
1022         var self = this,
1023             oldHelperLayer = document.querySelector('.introjs-helperLayer'),
1024             oldReferenceLayer = document.querySelector('.introjs-tooltipReferenceLayer'),
1025             highlightClass = 'introjs-helperLayer',
1026             nextTooltipButton,
1027             prevTooltipButton,
1028             skipTooltipButton,
1029             scrollParent;
1030
1031         //check for a current step highlight class
1032         if (typeof (targetElement.highlightClass) === 'string') {
1033             highlightClass += (' ' + targetElement.highlightClass);
1034         }
1035         //check for options highlight class
1036         if (typeof (this._options.highlightClass) === 'string') {
1037             highlightClass += (' ' + this._options.highlightClass);
1038         }
1039
1040         if (oldHelperLayer !== null) {
1041             var oldHelperNumberLayer = oldReferenceLayer.querySelector('.introjs-helperNumberLayer'),
1042                 oldtooltipLayer = oldReferenceLayer.querySelector('.introjs-tooltiptext'),
1043                 oldArrowLayer = oldReferenceLayer.querySelector('.introjs-arrow'),
1044                 oldtooltipContainer = oldReferenceLayer.querySelector('.introjs-tooltip');
1045
1046             skipTooltipButton = oldReferenceLayer.querySelector('.introjs-skipbutton');
1047             prevTooltipButton = oldReferenceLayer.querySelector('.introjs-prevbutton');
1048             nextTooltipButton = oldReferenceLayer.querySelector('.introjs-nextbutton');
1049
1050             //update or reset the helper highlight class
1051             oldHelperLayer.className = highlightClass;
1052             //hide the tooltip
1053             oldtooltipContainer.style.opacity = 0;
1054             oldtooltipContainer.style.display = "none";
1055
1056             if (oldHelperNumberLayer !== null) {
1057                 var lastIntroItem = this._introItems[(targetElement.step - 2 >= 0 ? targetElement.step - 2 : 0)];
1058
1059                 if (lastIntroItem !== null && (this._direction === 'forward' && lastIntroItem.position === 'floating') || (this._direction === 'backward' && targetElement.position === 'floating')) {
1060                     oldHelperNumberLayer.style.opacity = 0;
1061                 }
1062             }
1063
1064             // scroll to element
1065             scrollParent = _getScrollParent(targetElement.element);
1066
1067             if (scrollParent !== document.body) {
1068                 // target is within a scrollable element
1069                 _scrollParentToElement(scrollParent, targetElement.element);
1070             }
1071
1072             // set new position to helper layer
1073             _setHelperLayerPosition.call(self, oldHelperLayer);
1074             _setHelperLayerPosition.call(self, oldReferenceLayer);
1075
1076             //remove `introjs-fixParent` class from the elements
1077             var fixParents = document.querySelectorAll('.introjs-fixParent');
1078             _forEach(fixParents, function (parent) {
1079                 _removeClass(parent, /introjs-fixParent/g);
1080             });
1081
1082             //remove old classes if the element still exist
1083             _removeShowElement();
1084
1085             //we should wait until the CSS3 transition is competed (it's 0.3 sec) to prevent incorrect `height` and `width` calculation
1086             if (self._lastShowElementTimer) {
1087                 window.clearTimeout(self._lastShowElementTimer);
1088             }
1089
1090             self._lastShowElementTimer = window.setTimeout(function () {
1091                 //set current step to the label
1092                 if (oldHelperNumberLayer !== null) {
1093                     oldHelperNumberLayer.innerHTML = targetElement.step;
1094                 }
1095                 //set current tooltip text
1096                 oldtooltipLayer.innerHTML = targetElement.intro;
1097                 //set the tooltip position
1098                 oldtooltipContainer.style.display = "block";
1099                 _placeTooltip.call(self, targetElement.element, oldtooltipContainer, oldArrowLayer, oldHelperNumberLayer);
1100
1101                 //change active bullet
1102                 if (self._options.showBullets) {
1103                     oldReferenceLayer.querySelector('.introjs-bullets li > a.active').className = '';
1104                     oldReferenceLayer.querySelector('.introjs-bullets li > a[data-stepnumber="' + targetElement.step + '"]').className = 'active';
1105                 }
1106                 oldReferenceLayer.querySelector('.introjs-progress .introjs-progressbar').style.cssText = 'width:' + _getProgress.call(self) + '%;';
1107                 oldReferenceLayer.querySelector('.introjs-progress .introjs-progressbar').setAttribute('aria-valuenow', _getProgress.call(self));
1108
1109                 //show the tooltip
1110                 oldtooltipContainer.style.opacity = 1;
1111                 if (oldHelperNumberLayer) oldHelperNumberLayer.style.opacity = 1;
1112
1113                 //reset button focus
1114                 if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null && /introjs-donebutton/gi.test(skipTooltipButton.className)) {
1115                     // skip button is now "done" button
1116                     skipTooltipButton.focus();
1117                 } else if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1118                     //still in the tour, focus on next
1119                     nextTooltipButton.focus();
1120                 }
1121
1122                 // change the scroll of the window, if needed
1123                 _scrollTo.call(self, targetElement.scrollTo, targetElement, oldtooltipLayer);
1124             }, 350);
1125
1126             // end of old element if-else condition
1127         } else {
1128             var helperLayer = document.createElement('div'),
1129                 referenceLayer = document.createElement('div'),
1130                 arrowLayer = document.createElement('div'),
1131                 tooltipLayer = document.createElement('div'),
1132                 tooltipTextLayer = document.createElement('div'),
1133                 bulletsLayer = document.createElement('div'),
1134                 progressLayer = document.createElement('div'),
1135                 buttonsLayer = document.createElement('div');
1136
1137             helperLayer.className = highlightClass;
1138             referenceLayer.className = 'introjs-tooltipReferenceLayer';
1139
1140             // scroll to element
1141             scrollParent = _getScrollParent(targetElement.element);
1142
1143             if (scrollParent !== document.body) {
1144                 // target is within a scrollable element
1145                 _scrollParentToElement(scrollParent, targetElement.element);
1146             }
1147
1148             //set new position to helper layer
1149             _setHelperLayerPosition.call(self, helperLayer);
1150             _setHelperLayerPosition.call(self, referenceLayer);
1151
1152             //add helper layer to target element
1153             this._targetElement.appendChild(helperLayer);
1154             this._targetElement.appendChild(referenceLayer);
1155
1156             arrowLayer.className = 'introjs-arrow';
1157
1158             tooltipTextLayer.className = 'introjs-tooltiptext';
1159             tooltipTextLayer.innerHTML = targetElement.intro;
1160
1161             bulletsLayer.className = 'introjs-bullets';
1162
1163             if (this._options.showBullets === false) {
1164                 bulletsLayer.style.display = 'none';
1165             }
1166
1167             var ulContainer = document.createElement('ul');
1168             ulContainer.setAttribute('role', 'tablist');
1169
1170             var anchorClick = function () {
1171                 self.goToStep(this.getAttribute('data-stepnumber'));
1172             };
1173
1174             _forEach(this._introItems, function (item, i) {
1175                 var innerLi = document.createElement('li');
1176                 var anchorLink = document.createElement('a');
1177
1178                 innerLi.setAttribute('role', 'presentation');
1179                 anchorLink.setAttribute('role', 'tab');
1180
1181                 anchorLink.onclick = anchorClick;
1182
1183                 if (i === (targetElement.step - 1)) {
1184                     anchorLink.className = 'active';
1185                 }
1186
1187                 _setAnchorAsButton(anchorLink);
1188                 anchorLink.innerHTML = "&nbsp;";
1189                 anchorLink.setAttribute('data-stepnumber', item.step);
1190
1191                 innerLi.appendChild(anchorLink);
1192                 ulContainer.appendChild(innerLi);
1193             });
1194
1195             bulletsLayer.appendChild(ulContainer);
1196
1197             progressLayer.className = 'introjs-progress';
1198
1199             if (this._options.showProgress === false) {
1200                 progressLayer.style.display = 'none';
1201             }
1202             var progressBar = document.createElement('div');
1203             progressBar.className = 'introjs-progressbar';
1204             progressBar.setAttribute('role', 'progress');
1205             progressBar.setAttribute('aria-valuemin', 0);
1206             progressBar.setAttribute('aria-valuemax', 100);
1207             progressBar.setAttribute('aria-valuenow', _getProgress.call(this));
1208             progressBar.style.cssText = 'width:' + _getProgress.call(this) + '%;';
1209
1210             progressLayer.appendChild(progressBar);
1211
1212             buttonsLayer.className = 'introjs-tooltipbuttons';
1213             if (this._options.showButtons === false) {
1214                 buttonsLayer.style.display = 'none';
1215             }
1216
1217             tooltipLayer.className = 'introjs-tooltip';
1218             tooltipLayer.appendChild(tooltipTextLayer);
1219             tooltipLayer.appendChild(bulletsLayer);
1220             tooltipLayer.appendChild(progressLayer);
1221
1222             //add helper layer number
1223             var helperNumberLayer = document.createElement('span');
1224             if (this._options.showStepNumbers === true) {
1225                 helperNumberLayer.className = 'introjs-helperNumberLayer';
1226                 helperNumberLayer.innerHTML = targetElement.step;
1227                 referenceLayer.appendChild(helperNumberLayer);
1228             }
1229
1230             tooltipLayer.appendChild(arrowLayer);
1231             referenceLayer.appendChild(tooltipLayer);
1232
1233             //next button
1234             nextTooltipButton = document.createElement('a');
1235
1236             nextTooltipButton.onclick = function () {
1237                 if (self._introItems.length - 1 !== self._currentStep) {
1238                     _nextStep.call(self);
1239                 }
1240             };
1241
1242             _setAnchorAsButton(nextTooltipButton);
1243             nextTooltipButton.innerHTML = this._options.nextLabel;
1244
1245             //previous button
1246             prevTooltipButton = document.createElement('a');
1247
1248             prevTooltipButton.onclick = function () {
1249                 if (self._currentStep !== 0) {
1250                     _previousStep.call(self);
1251                 }
1252             };
1253
1254             _setAnchorAsButton(prevTooltipButton);
1255             prevTooltipButton.innerHTML = this._options.prevLabel;
1256
1257             //skip button
1258             skipTooltipButton = document.createElement('a');
1259             skipTooltipButton.className = this._options.buttonClass + ' introjs-skipbutton ';
1260             _setAnchorAsButton(skipTooltipButton);
1261             skipTooltipButton.innerHTML = this._options.skipLabel;
1262
1263             skipTooltipButton.onclick = function () {
1264                 if (self._introItems.length - 1 === self._currentStep && typeof (self._introCompleteCallback) === 'function') {
1265                     self._introCompleteCallback.call(self);
1266                 }
1267
1268                 if (self._introItems.length - 1 !== self._currentStep && typeof (self._introExitCallback) === 'function') {
1269                     self._introExitCallback.call(self);
1270                 }
1271
1272                 if (typeof (self._introSkipCallback) === 'function') {
1273                     self._introSkipCallback.call(self);
1274                 }
1275
1276                 _exitIntro.call(self, self._targetElement);
1277             };
1278
1279             buttonsLayer.appendChild(skipTooltipButton);
1280
1281             //in order to prevent displaying next/previous button always
1282             if (this._introItems.length > 1) {
1283                 buttonsLayer.appendChild(prevTooltipButton);
1284                 buttonsLayer.appendChild(nextTooltipButton);
1285             }
1286
1287             tooltipLayer.appendChild(buttonsLayer);
1288
1289             //set proper position
1290             _placeTooltip.call(self, targetElement.element, tooltipLayer, arrowLayer, helperNumberLayer);
1291
1292             // change the scroll of the window, if needed
1293             _scrollTo.call(this, targetElement.scrollTo, targetElement, tooltipLayer);
1294
1295             //end of new element if-else condition
1296         }
1297
1298         // removing previous disable interaction layer
1299         var disableInteractionLayer = self._targetElement.querySelector('.introjs-disableInteraction');
1300         if (disableInteractionLayer) {
1301             disableInteractionLayer.parentNode.removeChild(disableInteractionLayer);
1302         }
1303
1304         //disable interaction
1305         if (targetElement.disableInteraction) {
1306             _disableInteraction.call(self);
1307         }
1308
1309         // when it's the first step of tour
1310         if (this._currentStep === 0 && this._introItems.length > 1) {
1311             if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) {
1312                 skipTooltipButton.className = this._options.buttonClass + ' introjs-skipbutton';
1313             }
1314             if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1315                 nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton';
1316             }
1317
1318             if (this._options.hidePrev === true) {
1319                 if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) {
1320                     prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton introjs-hidden';
1321                 }
1322                 if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1323                     _addClass(nextTooltipButton, 'introjs-fullbutton');
1324                 }
1325             } else {
1326                 if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) {
1327                     prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton introjs-disabled';
1328                 }
1329             }
1330
1331             if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) {
1332                 skipTooltipButton.innerHTML = this._options.skipLabel;
1333             }
1334         } else if (this._introItems.length - 1 === this._currentStep || this._introItems.length === 1) {
1335             // last step of tour
1336             if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) {
1337                 skipTooltipButton.innerHTML = this._options.doneLabel;
1338                 // adding donebutton class in addition to skipbutton
1339                 _addClass(skipTooltipButton, 'introjs-donebutton');
1340             }
1341             if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) {
1342                 prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton';
1343             }
1344
1345             if (this._options.hideNext === true) {
1346                 if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1347                     nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton introjs-hidden';
1348                 }
1349                 if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) {
1350                     _addClass(prevTooltipButton, 'introjs-fullbutton');
1351                 }
1352             } else {
1353                 if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1354                     nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton introjs-disabled';
1355                 }
1356             }
1357         } else {
1358             // steps between start and end
1359             if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) {
1360                 skipTooltipButton.className = this._options.buttonClass + ' introjs-skipbutton';
1361             }
1362             if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) {
1363                 prevTooltipButton.className = this._options.buttonClass + ' introjs-prevbutton';
1364             }
1365             if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1366                 nextTooltipButton.className = this._options.buttonClass + ' introjs-nextbutton';
1367             }
1368             if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) {
1369                 skipTooltipButton.innerHTML = this._options.skipLabel;
1370             }
1371         }
1372
1373         prevTooltipButton.setAttribute('role', 'button');
1374         nextTooltipButton.setAttribute('role', 'button');
1375         skipTooltipButton.setAttribute('role', 'button');
1376
1377         //Set focus on "next" button, so that hitting Enter always moves you onto the next step
1378         if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) {
1379             nextTooltipButton.focus();
1380         }
1381
1382         _setShowElement(targetElement);
1383
1384         if (typeof (this._introAfterChangeCallback) !== 'undefined') {
1385             this._introAfterChangeCallback.call(this, targetElement.element);
1386         }
1387     }
1388
1389     /**
1390      * To change the scroll of `window` after highlighting an element
1391      *
1392      * @api private
1393      * @method _scrollTo
1394      * @param {String} scrollTo
1395      * @param {Object} targetElement
1396      * @param {Object} tooltipLayer
1397      */
1398     function _scrollTo(scrollTo, targetElement, tooltipLayer) {
1399         if (scrollTo === 'off') return;
1400         var rect;
1401
1402         if (!this._options.scrollToElement) return;
1403
1404         if (scrollTo === 'tooltip') {
1405             rect = tooltipLayer.getBoundingClientRect();
1406         } else {
1407             rect = targetElement.element.getBoundingClientRect();
1408         }
1409
1410         if (!_elementInViewport(targetElement.element)) {
1411             var winHeight = _getWinSize().height;
1412             var top = rect.bottom - (rect.bottom - rect.top);
1413
1414             // TODO (afshinm): do we need scroll padding now?
1415             // I have changed the scroll option and now it scrolls the window to
1416             // the center of the target element or tooltip.
1417
1418             if (top < 0 || targetElement.element.clientHeight > winHeight) {
1419                 window.scrollBy(0, rect.top - ((winHeight / 2) - (rect.height / 2)) - this._options.scrollPadding); // 30px padding from edge to look nice
1420
1421                 //Scroll down
1422             } else {
1423                 window.scrollBy(0, rect.top - ((winHeight / 2) - (rect.height / 2)) + this._options.scrollPadding); // 30px padding from edge to look nice
1424             }
1425         }
1426     }
1427
1428     /**
1429      * To remove all show element(s)
1430      *
1431      * @api private
1432      * @method _removeShowElement
1433      */
1434     function _removeShowElement() {
1435         var elms = document.querySelectorAll('.introjs-showElement');
1436
1437         _forEach(elms, function (elm) {
1438             _removeClass(elm, /introjs-[a-zA-Z]+/g);
1439         });
1440     }
1441
1442     /**
1443      * To set the show element
1444      * This function set a relative (in most cases) position and changes the z-index
1445      *
1446      * @api private
1447      * @method _setShowElement
1448      * @param {Object} targetElement
1449      */
1450     function _setShowElement(targetElement) {
1451         var parentElm;
1452         // we need to add this show element class to the parent of SVG elements
1453         // because the SVG elements can't have independent z-index
1454         if (targetElement.element instanceof SVGElement) {
1455             parentElm = targetElement.element.parentNode;
1456
1457             while (targetElement.element.parentNode !== null) {
1458                 if (!parentElm.tagName || parentElm.tagName.toLowerCase() === 'body') break;
1459
1460                 if (parentElm.tagName.toLowerCase() === 'svg') {
1461                     _addClass(parentElm, 'introjs-showElement introjs-relativePosition');
1462                 }
1463
1464                 parentElm = parentElm.parentNode;
1465             }
1466         }
1467
1468         _addClass(targetElement.element, 'introjs-showElement');
1469
1470         var currentElementPosition = _getPropValue(targetElement.element, 'position');
1471         if (currentElementPosition !== 'absolute' &&
1472             currentElementPosition !== 'relative' &&
1473             currentElementPosition !== 'fixed') {
1474             //change to new intro item
1475             _addClass(targetElement.element, 'introjs-relativePosition');
1476         }
1477
1478         parentElm = targetElement.element.parentNode;
1479         while (parentElm !== null) {
1480             if (!parentElm.tagName || parentElm.tagName.toLowerCase() === 'body') break;
1481
1482             //fix The Stacking Context problem.
1483             //More detail: https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context
1484             var zIndex = _getPropValue(parentElm, 'z-index');
1485             var opacity = parseFloat(_getPropValue(parentElm, 'opacity'));
1486             var transform = _getPropValue(parentElm, 'transform') || _getPropValue(parentElm, '-webkit-transform') || _getPropValue(parentElm, '-moz-transform') || _getPropValue(parentElm, '-ms-transform') || _getPropValue(parentElm, '-o-transform');
1487             if (/[0-9]+/.test(zIndex) || opacity < 1 || (transform !== 'none' && transform !== undefined)) {
1488                 _addClass(parentElm, 'introjs-fixParent');
1489             }
1490
1491             parentElm = parentElm.parentNode;
1492         }
1493     }
1494
1495     /**
1496      * Iterates arrays
1497      *
1498      * @param {Array} arr
1499      * @param {Function} forEachFnc
1500      * @param {Function} completeFnc
1501      * @return {Null}
1502      */
1503     function _forEach(arr, forEachFnc, completeFnc) {
1504         // in case arr is an empty query selector node list
1505         if (arr) {
1506             for (var i = 0, len = arr.length; i < len; i++) {
1507                 forEachFnc(arr[i], i);
1508             }
1509         }
1510
1511         if (typeof (completeFnc) === 'function') {
1512             completeFnc();
1513         }
1514     }
1515
1516     /**
1517      * Mark any object with an incrementing number
1518      * used for keeping track of objects
1519      *
1520      * @param Object obj   Any object or DOM Element
1521      * @param String key
1522      * @return Object
1523      */
1524     var _stamp = (function () {
1525         var keys = {};
1526         return function stamp(obj, key) {
1527
1528             // get group key
1529             key = key || 'introjs-stamp';
1530
1531             // each group increments from 0
1532             keys[key] = keys[key] || 0;
1533
1534             // stamp only once per object
1535             if (obj[key] === undefined) {
1536                 // increment key for each new object
1537                 obj[key] = keys[key]++;
1538             }
1539
1540             return obj[key];
1541         };
1542     })();
1543
1544     /**
1545      * DOMEvent Handles all DOM events
1546      *
1547      * methods:
1548      *
1549      * on - add event handler
1550      * off - remove event
1551      */
1552     var DOMEvent = (function () {
1553         function DOMEvent() {
1554             var events_key = 'introjs_event';
1555
1556             /**
1557              * Gets a unique ID for an event listener
1558              *
1559              * @param Object obj
1560              * @param String type        event type
1561              * @param Function listener
1562              * @param Object context
1563              * @return String
1564              */
1565             this._id = function (obj, type, listener, context) {
1566                 return type + _stamp(listener) + (context ? '_' + _stamp(context) : '');
1567             };
1568
1569             /**
1570              * Adds event listener
1571              *
1572              * @param Object obj
1573              * @param String type        event type
1574              * @param Function listener
1575              * @param Object context
1576              * @param Boolean useCapture
1577              * @return null
1578              */
1579             this.on = function (obj, type, listener, context, useCapture) {
1580                 var id = this._id.apply(this, arguments),
1581                     handler = function (e) {
1582                         return listener.call(context || obj, e || window.event);
1583                     };
1584
1585                 if ('addEventListener' in obj) {
1586                     obj.addEventListener(type, handler, useCapture);
1587                 } else if ('attachEvent' in obj) {
1588                     obj.attachEvent('on' + type, handler);
1589                 }
1590
1591                 obj[events_key] = obj[events_key] || {};
1592                 obj[events_key][id] = handler;
1593             };
1594
1595             /**
1596              * Removes event listener
1597              *
1598              * @param Object obj
1599              * @param String type        event type
1600              * @param Function listener
1601              * @param Object context
1602              * @param Boolean useCapture
1603              * @return null
1604              */
1605             this.off = function (obj, type, listener, context, useCapture) {
1606                 var id = this._id.apply(this, arguments),
1607                     handler = obj[events_key] && obj[events_key][id];
1608
1609                 if (!handler) {
1610                     return;
1611                 }
1612
1613                 if ('removeEventListener' in obj) {
1614                     obj.removeEventListener(type, handler, useCapture);
1615                 } else if ('detachEvent' in obj) {
1616                     obj.detachEvent('on' + type, handler);
1617                 }
1618
1619                 obj[events_key][id] = null;
1620             };
1621         }
1622
1623         return new DOMEvent();
1624     })();
1625
1626     /**
1627      * Append a class to an element
1628      *
1629      * @api private
1630      * @method _addClass
1631      * @param {Object} element
1632      * @param {String} className
1633      * @returns null
1634      */
1635     function _addClass(element, className) {
1636         if (element instanceof SVGElement) {
1637             // svg
1638             var pre = element.getAttribute('class') || '';
1639
1640             element.setAttribute('class', pre + ' ' + className);
1641         } else {
1642             if (element.classList !== undefined) {
1643                 // check for modern classList property
1644                 var classes = className.split(' ');
1645                 _forEach(classes, function (cls) {
1646                     element.classList.add(cls);
1647                 });
1648             } else if (!element.className.match(className)) {
1649                 // check if element doesn't already have className
1650                 element.className += ' ' + className;
1651             }
1652         }
1653     }
1654
1655     /**
1656      * Remove a class from an element
1657      *
1658      * @api private
1659      * @method _removeClass
1660      * @param {Object} element
1661      * @param {RegExp|String} classNameRegex can be regex or string
1662      * @returns null
1663      */
1664     function _removeClass(element, classNameRegex) {
1665         if (element instanceof SVGElement) {
1666             var pre = element.getAttribute('class') || '';
1667
1668             element.setAttribute('class', pre.replace(classNameRegex, '').replace(/^\s+|\s+$/g, ''));
1669         } else {
1670             element.className = element.className.replace(classNameRegex, '').replace(/^\s+|\s+$/g, '');
1671         }
1672     }
1673
1674     /**
1675      * Get an element CSS property on the page
1676      * Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml
1677      *
1678      * @api private
1679      * @method _getPropValue
1680      * @param {Object} element
1681      * @param {String} propName
1682      * @returns Element's property value
1683      */
1684     function _getPropValue(element, propName) {
1685         var propValue = '';
1686         if (element.currentStyle) { //IE
1687             propValue = element.currentStyle[propName];
1688         } else if (document.defaultView && document.defaultView.getComputedStyle) { //Others
1689             propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName);
1690         }
1691
1692         //Prevent exception in IE
1693         if (propValue && propValue.toLowerCase) {
1694             return propValue.toLowerCase();
1695         } else {
1696             return propValue;
1697         }
1698     }
1699
1700     /**
1701      * Checks to see if target element (or parents) position is fixed or not
1702      *
1703      * @api private
1704      * @method _isFixed
1705      * @param {Object} element
1706      * @returns Boolean
1707      */
1708     function _isFixed(element) {
1709         var p = element.parentNode;
1710
1711         if (!p || p.nodeName === 'HTML') {
1712             return false;
1713         }
1714
1715         if (_getPropValue(element, 'position') === 'fixed') {
1716             return true;
1717         }
1718
1719         return _isFixed(p);
1720     }
1721
1722     /**
1723      * Provides a cross-browser way to get the screen dimensions
1724      * via: http://stackoverflow.com/questions/5864467/internet-explorer-innerheight
1725      *
1726      * @api private
1727      * @method _getWinSize
1728      * @returns {Object} width and height attributes
1729      */
1730     function _getWinSize() {
1731         if (window.innerWidth !== undefined) {
1732             return {width: window.innerWidth, height: window.innerHeight};
1733         } else {
1734             var D = document.documentElement;
1735             return {width: D.clientWidth, height: D.clientHeight};
1736         }
1737     }
1738
1739     /**
1740      * Check to see if the element is in the viewport or not
1741      * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
1742      *
1743      * @api private
1744      * @method _elementInViewport
1745      * @param {Object} el
1746      */
1747     function _elementInViewport(el) {
1748         var rect = el.getBoundingClientRect();
1749
1750         return (
1751             rect.top >= 0 &&
1752             rect.left >= 0 &&
1753             (rect.bottom + 80) <= window.innerHeight && // add 80 to get the text right
1754             rect.right <= window.innerWidth
1755         );
1756     }
1757
1758     /**
1759      * Add overlay layer to the page
1760      *
1761      * @api private
1762      * @method _addOverlayLayer
1763      * @param {Object} targetElm
1764      */
1765     function _addOverlayLayer(targetElm) {
1766         var overlayLayer = document.createElement('div'),
1767             styleText = '',
1768             self = this;
1769
1770         //set css class name
1771         overlayLayer.className = 'introjs-overlay';
1772
1773         //check if the target element is body, we should calculate the size of overlay layer in a better way
1774         if (!targetElm.tagName || targetElm.tagName.toLowerCase() === 'body') {
1775             styleText += 'top: 0;bottom: 0; left: 0;right: 0;position: fixed;';
1776             overlayLayer.style.cssText = styleText;
1777         } else {
1778             //set overlay layer position
1779             var elementPosition = _getOffset(targetElm);
1780             if (elementPosition) {
1781                 styleText += 'width: ' + elementPosition.width + 'px; height:' + elementPosition.height + 'px; top:' + elementPosition.top + 'px;left: ' + elementPosition.left + 'px;';
1782                 overlayLayer.style.cssText = styleText;
1783             }
1784         }
1785
1786         targetElm.appendChild(overlayLayer);
1787
1788         overlayLayer.onclick = function () {
1789             if (self._options.exitOnOverlayClick === true) {
1790                 _exitIntro.call(self, targetElm);
1791             }
1792         };
1793
1794         window.setTimeout(function () {
1795             styleText += 'opacity: ' + self._options.overlayOpacity.toString() + ';';
1796             overlayLayer.style.cssText = styleText;
1797         }, 10);
1798
1799         return true;
1800     }
1801
1802     /**
1803      * Removes open hint (tooltip hint)
1804      *
1805      * @api private
1806      * @method _removeHintTooltip
1807      */
1808     function _removeHintTooltip() {
1809         var tooltip = document.querySelector('.introjs-hintReference');
1810
1811         if (tooltip) {
1812             var step = tooltip.getAttribute('data-step');
1813             tooltip.parentNode.removeChild(tooltip);
1814             return step;
1815         }
1816     }
1817
1818     /**
1819      * Start parsing hint items
1820      *
1821      * @api private
1822      * @param {Object} targetElm
1823      * @method _startHint
1824      */
1825     function _populateHints(targetElm) {
1826
1827         this._introItems = [];
1828
1829         if (this._options.hints) {
1830             _forEach(this._options.hints, function (hint) {
1831                 var currentItem = _cloneObject(hint);
1832
1833                 if (typeof (currentItem.element) === 'string') {
1834                     //grab the element with given selector from the page
1835                     currentItem.element = document.querySelector(currentItem.element);
1836                 }
1837
1838                 currentItem.hintPosition = currentItem.hintPosition || this._options.hintPosition;
1839                 currentItem.hintAnimation = currentItem.hintAnimation || this._options.hintAnimation;
1840
1841                 if (currentItem.element !== null) {
1842                     this._introItems.push(currentItem);
1843                 }
1844             }.bind(this));
1845         } else {
1846             var hints = targetElm.querySelectorAll('*[data-hint]');
1847
1848             if (!hints || !hints.length) {
1849                 return false;
1850             }
1851
1852             //first add intro items with data-step
1853             _forEach(hints, function (currentElement) {
1854                 // hint animation
1855                 var hintAnimation = currentElement.getAttribute('data-hintanimation');
1856
1857                 if (hintAnimation) {
1858                     hintAnimation = (hintAnimation === 'true');
1859                 } else {
1860                     hintAnimation = this._options.hintAnimation;
1861                 }
1862
1863                 this._introItems.push({
1864                     element: currentElement,
1865                     hint: currentElement.getAttribute('data-hint'),
1866                     hintPosition: currentElement.getAttribute('data-hintposition') || this._options.hintPosition,
1867                     hintAnimation: hintAnimation,
1868                     tooltipClass: currentElement.getAttribute('data-tooltipclass'),
1869                     position: currentElement.getAttribute('data-position') || this._options.tooltipPosition
1870                 });
1871             }.bind(this));
1872         }
1873
1874         _addHints.call(this);
1875
1876         /*
1877         todo:
1878         these events should be removed at some point
1879         */
1880         DOMEvent.on(document, 'click', _removeHintTooltip, this, false);
1881         DOMEvent.on(window, 'resize', _reAlignHints, this, true);
1882     }
1883
1884     /**
1885      * Re-aligns all hint elements
1886      *
1887      * @api private
1888      * @method _reAlignHints
1889      */
1890     function _reAlignHints() {
1891         _forEach(this._introItems, function (item) {
1892             if (typeof (item.targetElement) === 'undefined') {
1893                 return;
1894             }
1895
1896             _alignHintPosition.call(this, item.hintPosition, item.element, item.targetElement);
1897         }.bind(this));
1898     }
1899
1900     /**
1901      * Get a queryselector within the hint wrapper
1902      *
1903      * @param {String} selector
1904      * @return {NodeList|Array}
1905      */
1906     function _hintQuerySelectorAll(selector) {
1907         var hintsWrapper = document.querySelector('.introjs-hints');
1908         return (hintsWrapper) ? hintsWrapper.querySelectorAll(selector) : [];
1909     }
1910
1911     /**
1912      * Hide a hint
1913      *
1914      * @api private
1915      * @method _hideHint
1916      */
1917     function _hideHint(stepId) {
1918         var hint = _hintQuerySelectorAll('.introjs-hint[data-step="' + stepId + '"]')[0];
1919
1920         _removeHintTooltip.call(this);
1921
1922         if (hint) {
1923             _addClass(hint, 'introjs-hidehint');
1924         }
1925
1926         // call the callback function (if any)
1927         if (typeof (this._hintCloseCallback) !== 'undefined') {
1928             this._hintCloseCallback.call(this, stepId);
1929         }
1930     }
1931
1932     /**
1933      * Hide all hints
1934      *
1935      * @api private
1936      * @method _hideHints
1937      */
1938     function _hideHints() {
1939         var hints = _hintQuerySelectorAll('.introjs-hint');
1940
1941         _forEach(hints, function (hint) {
1942             _hideHint.call(this, hint.getAttribute('data-step'));
1943         }.bind(this));
1944     }
1945
1946     /**
1947      * Show all hints
1948      *
1949      * @api private
1950      * @method _showHints
1951      */
1952     function _showHints() {
1953         var hints = _hintQuerySelectorAll('.introjs-hint');
1954
1955         if (hints && hints.length) {
1956             _forEach(hints, function (hint) {
1957                 _showHint.call(this, hint.getAttribute('data-step'));
1958             }.bind(this));
1959         } else {
1960             _populateHints.call(this, this._targetElement);
1961         }
1962     }
1963
1964     /**
1965      * Show a hint
1966      *
1967      * @api private
1968      * @method _showHint
1969      */
1970     function _showHint(stepId) {
1971         var hint = _hintQuerySelectorAll('.introjs-hint[data-step="' + stepId + '"]')[0];
1972
1973         if (hint) {
1974             _removeClass(hint, /introjs-hidehint/g);
1975         }
1976     }
1977
1978     /**
1979      * Removes all hint elements on the page
1980      * Useful when you want to destroy the elements and add them again (e.g. a modal or popup)
1981      *
1982      * @api private
1983      * @method _removeHints
1984      */
1985     function _removeHints() {
1986         var hints = _hintQuerySelectorAll('.introjs-hint');
1987
1988         _forEach(hints, function (hint) {
1989             _removeHint.call(this, hint.getAttribute('data-step'));
1990         }.bind(this));
1991     }
1992
1993     /**
1994      * Remove one single hint element from the page
1995      * Useful when you want to destroy the element and add them again (e.g. a modal or popup)
1996      * Use removeHints if you want to remove all elements.
1997      *
1998      * @api private
1999      * @method _removeHint
2000      */
2001     function _removeHint(stepId) {
2002         var hint = _hintQuerySelectorAll('.introjs-hint[data-step="' + stepId + '"]')[0];
2003
2004         if (hint) {
2005             hint.parentNode.removeChild(hint);
2006         }
2007     }
2008
2009     /**
2010      * Add all available hints to the page
2011      *
2012      * @api private
2013      * @method _addHints
2014      */
2015     function _addHints() {
2016         var self = this;
2017
2018         var hintsWrapper = document.querySelector('.introjs-hints');
2019
2020         if (hintsWrapper === null) {
2021             hintsWrapper = document.createElement('div');
2022             hintsWrapper.className = 'introjs-hints';
2023         }
2024
2025         /**
2026          * Returns an event handler unique to the hint iteration
2027          *
2028          * @param {Integer} i
2029          * @return {Function}
2030          */
2031         var getHintClick = function (i) {
2032             return function (e) {
2033                 var evt = e ? e : window.event;
2034
2035                 if (evt.stopPropagation) {
2036                     evt.stopPropagation();
2037                 }
2038
2039                 if (evt.cancelBubble !== null) {
2040                     evt.cancelBubble = true;
2041                 }
2042
2043                 _showHintDialog.call(self, i);
2044             };
2045         };
2046
2047         _forEach(this._introItems, function (item, i) {
2048             // avoid append a hint twice
2049             if (document.querySelector('.introjs-hint[data-step="' + i + '"]')) {
2050                 return;
2051             }
2052
2053             var hint = document.createElement('a');
2054             _setAnchorAsButton(hint);
2055
2056             hint.onclick = getHintClick(i);
2057
2058             hint.className = 'introjs-hint';
2059
2060             if (!item.hintAnimation) {
2061                 _addClass(hint, 'introjs-hint-no-anim');
2062             }
2063
2064             // hint's position should be fixed if the target element's position is fixed
2065             if (_isFixed(item.element)) {
2066                 _addClass(hint, 'introjs-fixedhint');
2067             }
2068
2069             var hintDot = document.createElement('div');
2070             hintDot.className = 'introjs-hint-dot';
2071             var hintPulse = document.createElement('div');
2072             hintPulse.className = 'introjs-hint-pulse';
2073
2074             hint.appendChild(hintDot);
2075             hint.appendChild(hintPulse);
2076             hint.setAttribute('data-step', i);
2077
2078             // we swap the hint element with target element
2079             // because _setHelperLayerPosition uses `element` property
2080             item.targetElement = item.element;
2081             item.element = hint;
2082
2083             // align the hint position
2084             _alignHintPosition.call(this, item.hintPosition, hint, item.targetElement);
2085
2086             hintsWrapper.appendChild(hint);
2087         }.bind(this));
2088
2089         // adding the hints wrapper
2090         document.body.appendChild(hintsWrapper);
2091
2092         // call the callback function (if any)
2093         if (typeof (this._hintsAddedCallback) !== 'undefined') {
2094             this._hintsAddedCallback.call(this);
2095         }
2096     }
2097
2098     /**
2099      * Aligns hint position
2100      *
2101      * @api private
2102      * @method _alignHintPosition
2103      * @param {String} position
2104      * @param {Object} hint
2105      * @param {Object} element
2106      */
2107     function _alignHintPosition(position, hint, element) {
2108         // get/calculate offset of target element
2109         var offset = _getOffset.call(this, element);
2110         var iconWidth = 20;
2111         var iconHeight = 20;
2112
2113         // align the hint element
2114         switch (position) {
2115             default:
2116             case 'top-left':
2117                 hint.style.left = offset.left + 'px';
2118                 hint.style.top = offset.top + 'px';
2119                 break;
2120             case 'top-right':
2121                 hint.style.left = (offset.left + offset.width - iconWidth) + 'px';
2122                 hint.style.top = offset.top + 'px';
2123                 break;
2124             case 'bottom-left':
2125                 hint.style.left = offset.left + 'px';
2126                 hint.style.top = (offset.top + offset.height - iconHeight) + 'px';
2127                 break;
2128             case 'bottom-right':
2129                 hint.style.left = (offset.left + offset.width - iconWidth) + 'px';
2130                 hint.style.top = (offset.top + offset.height - iconHeight) + 'px';
2131                 break;
2132             case 'middle-left':
2133                 hint.style.left = offset.left + 'px';
2134                 hint.style.top = (offset.top + (offset.height - iconHeight) / 2) + 'px';
2135                 break;
2136             case 'middle-right':
2137                 hint.style.left = (offset.left + offset.width - iconWidth) + 'px';
2138                 hint.style.top = (offset.top + (offset.height - iconHeight) / 2) + 'px';
2139                 break;
2140             case 'middle-middle':
2141                 hint.style.left = (offset.left + (offset.width - iconWidth) / 2) + 'px';
2142                 hint.style.top = (offset.top + (offset.height - iconHeight) / 2) + 'px';
2143                 break;
2144             case 'bottom-middle':
2145                 hint.style.left = (offset.left + (offset.width - iconWidth) / 2) + 'px';
2146                 hint.style.top = (offset.top + offset.height - iconHeight) + 'px';
2147                 break;
2148             case 'top-middle':
2149                 hint.style.left = (offset.left + (offset.width - iconWidth) / 2) + 'px';
2150                 hint.style.top = offset.top + 'px';
2151                 break;
2152         }
2153     }
2154
2155     /**
2156      * Triggers when user clicks on the hint element
2157      *
2158      * @api private
2159      * @method _showHintDialog
2160      * @param {Number} stepId
2161      */
2162     function _showHintDialog(stepId) {
2163         var hintElement = document.querySelector('.introjs-hint[data-step="' + stepId + '"]');
2164         var item = this._introItems[stepId];
2165
2166         // call the callback function (if any)
2167         if (typeof (this._hintClickCallback) !== 'undefined') {
2168             this._hintClickCallback.call(this, hintElement, item, stepId);
2169         }
2170
2171         // remove all open tooltips
2172         var removedStep = _removeHintTooltip.call(this);
2173
2174         // to toggle the tooltip
2175         if (parseInt(removedStep, 10) === stepId) {
2176             return;
2177         }
2178
2179         var tooltipLayer = document.createElement('div');
2180         var tooltipTextLayer = document.createElement('div');
2181         var arrowLayer = document.createElement('div');
2182         var referenceLayer = document.createElement('div');
2183
2184         tooltipLayer.className = 'introjs-tooltip';
2185
2186         tooltipLayer.onclick = function (e) {
2187             //IE9 & Other Browsers
2188             if (e.stopPropagation) {
2189                 e.stopPropagation();
2190             }
2191             //IE8 and Lower
2192             else {
2193                 e.cancelBubble = true;
2194             }
2195         };
2196
2197         tooltipTextLayer.className = 'introjs-tooltiptext';
2198
2199         var tooltipWrapper = document.createElement('p');
2200         tooltipWrapper.innerHTML = item.hint;
2201
2202         var closeButton = document.createElement('a');
2203         closeButton.className = this._options.buttonClass;
2204         closeButton.setAttribute('role', 'button');
2205         closeButton.innerHTML = this._options.hintButtonLabel;
2206         closeButton.onclick = _hideHint.bind(this, stepId);
2207
2208         tooltipTextLayer.appendChild(tooltipWrapper);
2209         tooltipTextLayer.appendChild(closeButton);
2210
2211         arrowLayer.className = 'introjs-arrow';
2212         tooltipLayer.appendChild(arrowLayer);
2213
2214         tooltipLayer.appendChild(tooltipTextLayer);
2215
2216         // set current step for _placeTooltip function
2217         this._currentStep = hintElement.getAttribute('data-step');
2218
2219         // align reference layer position
2220         referenceLayer.className = 'introjs-tooltipReferenceLayer introjs-hintReference';
2221         referenceLayer.setAttribute('data-step', hintElement.getAttribute('data-step'));
2222         _setHelperLayerPosition.call(this, referenceLayer);
2223
2224         referenceLayer.appendChild(tooltipLayer);
2225         document.body.appendChild(referenceLayer);
2226
2227         //set proper position
2228         _placeTooltip.call(this, hintElement, tooltipLayer, arrowLayer, null, true);
2229     }
2230
2231     /**
2232      * Get an element position on the page
2233      * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966
2234      *
2235      * @api private
2236      * @method _getOffset
2237      * @param {Object} element
2238      * @returns Element's position info
2239      */
2240     function _getOffset(element) {
2241         var body = document.body;
2242         var docEl = document.documentElement;
2243         var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
2244         var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
2245         var x = element.getBoundingClientRect();
2246         return {
2247             top: x.top + scrollTop,
2248             width: x.width,
2249             height: x.height,
2250             left: x.left + scrollLeft
2251         };
2252     }
2253
2254     /**
2255      * Find the nearest scrollable parent
2256      * copied from https://stackoverflow.com/questions/35939886/find-first-scrollable-parent
2257      *
2258      * @param Element element
2259      * @return Element
2260      */
2261     function _getScrollParent(element) {
2262         var style = window.getComputedStyle(element);
2263         var excludeStaticParent = (style.position === "absolute");
2264         var overflowRegex = /(auto|scroll)/;
2265
2266         if (style.position === "fixed") return document.body;
2267
2268         for (var parent = element; (parent = parent.parentElement);) {
2269             style = window.getComputedStyle(parent);
2270             if (excludeStaticParent && style.position === "static") {
2271                 continue;
2272             }
2273             if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
2274         }
2275
2276         return document.body;
2277     }
2278
2279     /**
2280      * scroll a scrollable element to a child element
2281      *
2282      * @param Element parent
2283      * @param Element element
2284      * @return Null
2285      */
2286     function _scrollParentToElement(parent, element) {
2287         parent.scrollTop = element.offsetTop - parent.offsetTop;
2288     }
2289
2290     /**
2291      * Gets the current progress percentage
2292      *
2293      * @api private
2294      * @method _getProgress
2295      * @returns current progress percentage
2296      */
2297     function _getProgress() {
2298         // Steps are 0 indexed
2299         var currentStep = parseInt((this._currentStep + 1), 10);
2300         return ((currentStep / this._introItems.length) * 100);
2301     }
2302
2303     /**
2304      * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
2305      * via: http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
2306      *
2307      * @param obj1
2308      * @param obj2
2309      * @returns obj3 a new object based on obj1 and obj2
2310      */
2311     function _mergeOptions(obj1, obj2) {
2312         var obj3 = {},
2313             attrname;
2314         for (attrname in obj1) {
2315             obj3[attrname] = obj1[attrname];
2316         }
2317         for (attrname in obj2) {
2318             obj3[attrname] = obj2[attrname];
2319         }
2320         return obj3;
2321     }
2322
2323     var introJs = function (targetElm) {
2324         var instance;
2325
2326         if (typeof (targetElm) === 'object') {
2327             //Ok, create a new instance
2328             instance = new IntroJs(targetElm);
2329
2330         } else if (typeof (targetElm) === 'string') {
2331             //select the target element with query selector
2332             var targetElement = document.querySelector(targetElm);
2333
2334             if (targetElement) {
2335                 instance = new IntroJs(targetElement);
2336             } else {
2337                 throw new Error('There is no element with given selector.');
2338             }
2339         } else {
2340             instance = new IntroJs(document.body);
2341         }
2342         // add instance to list of _instances
2343         // passing group to _stamp to increment
2344         // from 0 onward somewhat reliably
2345         introJs.instances[_stamp(instance, 'introjs-instance')] = instance;
2346
2347         return instance;
2348     };
2349
2350     /**
2351      * Current IntroJs version
2352      *
2353      * @property version
2354      * @type String
2355      */
2356     introJs.version = VERSION;
2357
2358     /**
2359      * key-val object helper for introJs instances
2360      *
2361      * @property instances
2362      * @type Object
2363      */
2364     introJs.instances = {};
2365
2366     //Prototype
2367     introJs.fn = IntroJs.prototype = {
2368         clone: function () {
2369             return new IntroJs(this);
2370         },
2371         setOption: function (option, value) {
2372             this._options[option] = value;
2373             return this;
2374         },
2375         setOptions: function (options) {
2376             this._options = _mergeOptions(this._options, options);
2377             return this;
2378         },
2379         start: function (group) {
2380             _introForElement.call(this, this._targetElement, group);
2381             return this;
2382         },
2383         goToStep: function (step) {
2384             _goToStep.call(this, step);
2385             return this;
2386         },
2387         addStep: function (options) {
2388             if (!this._options.steps) {
2389                 this._options.steps = [];
2390             }
2391
2392             this._options.steps.push(options);
2393
2394             return this;
2395         },
2396         addSteps: function (steps) {
2397             if (!steps.length) return;
2398
2399             for (var index = 0; index < steps.length; index++) {
2400                 this.addStep(steps[index]);
2401             }
2402
2403             return this;
2404         },
2405         goToStepNumber: function (step) {
2406             _goToStepNumber.call(this, step);
2407
2408             return this;
2409         },
2410         nextStep: function () {
2411             _nextStep.call(this);
2412             return this;
2413         },
2414         previousStep: function () {
2415             _previousStep.call(this);
2416             return this;
2417         },
2418         exit: function (force) {
2419             _exitIntro.call(this, this._targetElement, force);
2420             return this;
2421         },
2422         refresh: function () {
2423             _refresh.call(this);
2424             return this;
2425         },
2426         onbeforechange: function (providedCallback) {
2427             if (typeof (providedCallback) === 'function') {
2428                 this._introBeforeChangeCallback = providedCallback;
2429             } else {
2430                 throw new Error('Provided callback for onbeforechange was not a function');
2431             }
2432             return this;
2433         },
2434         onchange: function (providedCallback) {
2435             if (typeof (providedCallback) === 'function') {
2436                 this._introChangeCallback = providedCallback;
2437             } else {
2438                 throw new Error('Provided callback for onchange was not a function.');
2439             }
2440             return this;
2441         },
2442         onafterchange: function (providedCallback) {
2443             if (typeof (providedCallback) === 'function') {
2444                 this._introAfterChangeCallback = providedCallback;
2445             } else {
2446                 throw new Error('Provided callback for onafterchange was not a function');
2447             }
2448             return this;
2449         },
2450         oncomplete: function (providedCallback) {
2451             if (typeof (providedCallback) === 'function') {
2452                 this._introCompleteCallback = providedCallback;
2453             } else {
2454                 throw new Error('Provided callback for oncomplete was not a function.');
2455             }
2456             return this;
2457         },
2458         onhintsadded: function (providedCallback) {
2459             if (typeof (providedCallback) === 'function') {
2460                 this._hintsAddedCallback = providedCallback;
2461             } else {
2462                 throw new Error('Provided callback for onhintsadded was not a function.');
2463             }
2464             return this;
2465         },
2466         onhintclick: function (providedCallback) {
2467             if (typeof (providedCallback) === 'function') {
2468                 this._hintClickCallback = providedCallback;
2469             } else {
2470                 throw new Error('Provided callback for onhintclick was not a function.');
2471             }
2472             return this;
2473         },
2474         onhintclose: function (providedCallback) {
2475             if (typeof (providedCallback) === 'function') {
2476                 this._hintCloseCallback = providedCallback;
2477             } else {
2478                 throw new Error('Provided callback for onhintclose was not a function.');
2479             }
2480             return this;
2481         },
2482         onexit: function (providedCallback) {
2483             if (typeof (providedCallback) === 'function') {
2484                 this._introExitCallback = providedCallback;
2485             } else {
2486                 throw new Error('Provided callback for onexit was not a function.');
2487             }
2488             return this;
2489         },
2490         onskip: function (providedCallback) {
2491             if (typeof (providedCallback) === 'function') {
2492                 this._introSkipCallback = providedCallback;
2493             } else {
2494                 throw new Error('Provided callback for onskip was not a function.');
2495             }
2496             return this;
2497         },
2498         onbeforeexit: function (providedCallback) {
2499             if (typeof (providedCallback) === 'function') {
2500                 this._introBeforeExitCallback = providedCallback;
2501             } else {
2502                 throw new Error('Provided callback for onbeforeexit was not a function.');
2503             }
2504             return this;
2505         },
2506         addHints: function () {
2507             _populateHints.call(this, this._targetElement);
2508             return this;
2509         },
2510         hideHint: function (stepId) {
2511             _hideHint.call(this, stepId);
2512             return this;
2513         },
2514         hideHints: function () {
2515             _hideHints.call(this);
2516             return this;
2517         },
2518         showHint: function (stepId) {
2519             _showHint.call(this, stepId);
2520             return this;
2521         },
2522         showHints: function () {
2523             _showHints.call(this);
2524             return this;
2525         },
2526         removeHints: function () {
2527             _removeHints.call(this);
2528             return this;
2529         },
2530         removeHint: function (stepId) {
2531             _removeHint.call(this, stepId);
2532             return this;
2533         },
2534         showHintDialog: function (stepId) {
2535             _showHintDialog.call(this, stepId);
2536             return this;
2537         }
2538     };
2539
2540     return introJs;
2541 });