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