function guideJs() {
  function GuideJs(obj) {
    this._targetElement = obj;
    this._guideItems = [];

    this._options = {
      nextLabel: 'Next;',
      prevLabel: 'Back',
      skipLabel: 'Skip',
      doneLabel: 'Done',
      hidePrev: false,
      hideNext: false,
      tooltipPosition: 'bottom',
      tooltipClass: '',
      highlightClass: '',
      exitOnEsc: true,
      exitOnOverlayClick: true,
      showStepNumbers: true,
      keyboardNavigation: true,
      showButtons: true,
      showBullets: true,
      showProgress: false,
      scrollToElement: true,
      scrollTo: 'element',
      scrollPadding: 30,
      overlayOpacity: 0.8,
      positionPrecedence: ['bottom', 'top', 'right', 'left'],
      disableInteraction: false,
      helperElementPadding: 10,
      buttonClass: 'guidejs-button',
    };
  }

  function _guideForElement(targetElm, group) {
    let allGuideSteps = targetElm.querySelectorAll('*[data-guide]'),
      guideItems: any = [];

    if (this._options.steps) {
      _forEach(
        this._options.steps,
        function (step) {
          const currentItem = _cloneObject(step);

          currentItem.step = guideItems.length + 1;

          if (typeof currentItem.element === 'string') {
            currentItem.element = document.querySelector(currentItem.element);
          }

          if (typeof currentItem.element === 'undefined' || currentItem.element === null) {
            let floatingElementQuery = document.querySelector('.guidejsFloatingElement');

            if (floatingElementQuery === null) {
              floatingElementQuery = document.createElement('div');
              floatingElementQuery.className = 'guidejsFloatingElement';

              document.body.appendChild(floatingElementQuery);
            }

            currentItem.element = floatingElementQuery;
            currentItem.position = 'floating';
          }

          currentItem.scrollTo = currentItem.scrollTo || this._options.scrollTo;

          if (typeof currentItem.disableInteraction === 'undefined') {
            currentItem.disableInteraction = this._options.disableInteraction;
          }

          if (currentItem.element !== null) {
            guideItems.push(currentItem);
          }
        }.bind(this),
      );
    } else {
      const elmsLength = allGuideSteps.length;
      let disableInteraction;

      if (elmsLength < 1) {
        return false;
      }

      _forEach(
        allGuideSteps,
        function (currentElement) {
          if (group && currentElement.getAttribute('data-guide-group') !== group) {
            return;
          }

          if (currentElement.style.display === 'none') {
            return;
          }

          const step = parseInt(currentElement.getAttribute('data-step'), 10);

          if (typeof currentElement.getAttribute('data-disable-interaction') !== 'undefined') {
            disableInteraction = !!currentElement.getAttribute('data-disable-interaction');
          } else {
            disableInteraction = this._options.disableInteraction;
          }

          if (step > 0) {
            guideItems[step - 1] = {
              element: currentElement,
              guide: currentElement.getAttribute('data-guide'),
              step: parseInt(currentElement.getAttribute('data-step'), 10),
              tooltipClass: currentElement.getAttribute('data-tooltipclass'),
              highlightClass: currentElement.getAttribute('data-highlightclass'),
              position:
                currentElement.getAttribute('data-position') || this._options.tooltipPosition,
              scrollTo: currentElement.getAttribute('data-scrollto') || this._options.scrollTo,
              disableInteraction: disableInteraction,
            };
          }
        }.bind(this),
      );

      let nextStep = 0;

      _forEach(
        allGuideSteps,
        function (currentElement) {
          if (group && currentElement.getAttribute('data-guide-group') !== group) {
            return;
          }

          if (currentElement.getAttribute('data-step') === null) {
            while (true) {
              if (typeof guideItems[nextStep] === 'undefined') {
                break;
              } else {
                nextStep++;
              }
            }

            if (typeof currentElement.getAttribute('data-disable-interaction') !== 'undefined') {
              disableInteraction = !!currentElement.getAttribute('data-disable-interaction');
            } else {
              disableInteraction = this._options.disableInteraction;
            }

            guideItems[nextStep] = {
              element: currentElement,
              guide: currentElement.getAttribute('data-guide'),
              step: nextStep + 1,
              tooltipClass: currentElement.getAttribute('data-tooltipclass'),
              highlightClass: currentElement.getAttribute('data-highlightclass'),
              position:
                currentElement.getAttribute('data-position') || this._options.tooltipPosition,
              scrollTo: currentElement.getAttribute('data-scrollto') || this._options.scrollTo,
              disableInteraction: disableInteraction,
            };
          }
        }.bind(this),
      );
    }

    const tempGuideItems: any = [];
    for (let z = 0; z < guideItems.length; z++) {
      if (guideItems[z]) {
        tempGuideItems.push(guideItems[z]);
      }
    }

    guideItems = tempGuideItems;

    guideItems.sort(function (a, b) {
      return a.step - b.step;
    });

    this._guideItems = guideItems;

    if (_addOverlayLayer.call(this, targetElm)) {
      _nextStep.call(this);

      if (this._options.keyboardNavigation) {
        DOMEvent.on(window, 'keydown', _onKeyDown, this, true);
      }
      DOMEvent.on(window, 'resize', _onResize, this, true);
    }
    return false;
  }

  function _onResize() {
    this.refresh.call(this);
  }

  // todo
  function _onKeyDown(e) {
    let code = e.code === null ? e.which : e.code;

    if (code === null) {
      code = e.charCode === null ? e.keyCode : e.charCode;
    }
    if ((code === 'Escape' || code === 27) && this._options.exitOnEsc === true) {
      _exitGuide.call(this, this._targetElement);
    } else if (code === 'ArrowLeft' || code === 37) {
      _previousStep.call(this);
    } else if (code === 'ArrowRight' || code === 39) {
      _nextStep.call(this);
    } else if (code === 'Enter' || code === 13) {
      const target = e.target || e.srcElement;
      if (target && target.className.match('guidejs-prevbutton')) {
        _previousStep.call(this);
      } else if (target && target.className.match('guidejs-skipbutton')) {
        if (
          this._guideItems.length - 1 === this._currentStep &&
          typeof this._guideCompleteCallback === 'function'
        ) {
          this._guideCompleteCallback.call(this);
        }

        _exitGuide.call(this, this._targetElement);
      } else if (target && target.getAttribute('data-stepnumber')) {
        target.click();
      } else {
        _nextStep.call(this);
      }

      if (e.preventDefault) {
        e.preventDefault();
      } else {
        e.returnValue = false;
      }
    }
  }

  function _cloneObject(object) {
    if (object === null || typeof object !== 'object' || typeof object.nodeType !== 'undefined') {
      return object;
    }
    const temp = {};
    for (const key in object) {
      if (typeof window.jQuery !== 'undefined' && object[key] instanceof window.jQuery) {
        temp[key] = object[key];
      } else {
        temp[key] = _cloneObject(object[key]);
      }
    }
    return temp;
  }

  function _goToStep(step) {
    this._currentStep = step - 2;
    if (typeof this._guideItems !== 'undefined') {
      _nextStep.call(this);
    }
  }

  function _goToStepNumber(step) {
    this._currentStepNumber = step;
    if (typeof this._guideItems !== 'undefined') {
      _nextStep.call(this);
    }
  }

  function _nextStep() {
    this._direction = 'forward';

    if (typeof this._currentStepNumber !== 'undefined') {
      _forEach(
        this._guideItems,
        function (item, i) {
          if (item.step === this._currentStepNumber) {
            this._currentStep = i - 1;
            this._currentStepNumber = undefined;
          }
        }.bind(this),
      );
    }

    if (typeof this._currentStep === 'undefined') {
      this._currentStep = 0;
    } else {
      ++this._currentStep;
    }

    const nextStep = this._guideItems[this._currentStep];
    let continueStep = true;

    if (typeof this._guideBeforeChangeCallback !== 'undefined') {
      continueStep = this._guideBeforeChangeCallback.call(this, nextStep.element);
    }

    if (continueStep === false) {
      --this._currentStep;
      return false;
    }

    if (this._guideItems.length <= this._currentStep) {
      if (typeof this._guideCompleteCallback === 'function') {
        this._guideCompleteCallback.call(this);
      }
      _exitGuide.call(this, this._targetElement);
      return;
    }

    _showElement.call(this, nextStep);
  }

  function _previousStep() {
    this._direction = 'backward';

    if (this._currentStep === 0) {
      return false;
    }

    --this._currentStep;

    const nextStep = this._guideItems[this._currentStep];
    let continueStep = true;

    if (typeof this._guideBeforeChangeCallback !== 'undefined') {
      continueStep = this._guideBeforeChangeCallback.call(this, nextStep.element);
    }

    if (continueStep === false) {
      ++this._currentStep;
      return false;
    }

    _showElement.call(this, nextStep);
  }

  function _refresh() {
    _setHelperLayerPosition.call(this, document.querySelector('.guidejs-helperLayer'));
    _setHelperLayerPosition.call(this, document.querySelector('.guidejs-tooltipReferenceLayer'));
    _setHelperLayerPosition.call(this, document.querySelector('.guidejs-disableInteraction'));

    if (this._currentStep !== undefined && this._currentStep !== null) {
      const oldHelperNumberLayer = document.querySelector('.guidejs-helperNumberLayer'),
        oldArrowLayer = document.querySelector('.guidejs-arrow'),
        oldtooltipContainer = document.querySelector('.guidejs-tooltip');
      _placeTooltip.call(
        this,
        this._guideItems[this._currentStep].element,
        oldtooltipContainer,
        oldArrowLayer,
        oldHelperNumberLayer,
      );
    }
    return this;
  }

  function _exitGuide(targetElement, force) {
    let continueExit = true;
    if (this._guideBeforeExitCallback !== undefined) {
      continueExit = this._guideBeforeExitCallback.call(this);
    }
    if (!force && continueExit === false) return;

    const overlayLayers = targetElement.querySelectorAll('.guidejs-overlay');

    if (overlayLayers && overlayLayers.length) {
      _forEach(
        overlayLayers,
        function (overlayLayer) {
          overlayLayer.style.opacity = 0;
          window.setTimeout(
            function () {
              if (this.parentNode) {
                this.parentNode.removeChild(this);
              }
            }.bind(overlayLayer),
            500,
          );
        }.bind(this),
      );
    }

    const helperLayer = targetElement.querySelector('.guidejs-helperLayer');
    if (helperLayer) {
      helperLayer.parentNode.removeChild(helperLayer);
    }

    const referenceLayer = targetElement.querySelector('.guidejs-tooltipReferenceLayer');
    if (referenceLayer) {
      referenceLayer.parentNode.removeChild(referenceLayer);
    }

    const disableInteractionLayer = targetElement.querySelector('.guidejs-disableInteraction');
    if (disableInteractionLayer) {
      disableInteractionLayer.parentNode.removeChild(disableInteractionLayer);
    }

    const floatingElement = document.querySelector('.guidejsFloatingElement');
    if (floatingElement) {
      (floatingElement as any).parentNode.removeChild(floatingElement);
    }

    _removeShowElement();

    const fixParents = document.querySelectorAll('.guidejs-fixParent');
    _forEach(fixParents, function (parent) {
      _removeClass(parent, /guidejs-fixParent/g);
    });

    DOMEvent.off(window, 'keydown', _onKeyDown, this, true);
    DOMEvent.off(window, 'resize', _onResize, this, true);

    if (this._guideExitCallback !== undefined) {
      this._guideExitCallback.call(this);
    }

    this._currentStep = undefined;
  }

  function _placeTooltip(targetElement, tooltipLayer, arrowLayer, helperNumberLayer) {
    let tooltipCssClass = '',
      currentStepObj,
      tooltipOffset,
      targetOffset,
      windowSize,
      currentTooltipPosition;

    tooltipLayer.style.top = null;
    tooltipLayer.style.right = null;
    tooltipLayer.style.bottom = null;
    tooltipLayer.style.left = null;
    tooltipLayer.style.marginLeft = null;
    tooltipLayer.style.marginTop = null;

    arrowLayer.style.display = 'inherit';

    if (typeof helperNumberLayer !== 'undefined' && helperNumberLayer !== null) {
      helperNumberLayer.style.top = null;
      helperNumberLayer.style.left = null;
    }

    if (!this._guideItems[this._currentStep]) return;

    currentStepObj = this._guideItems[this._currentStep];
    if (typeof currentStepObj.tooltipClass === 'string') {
      tooltipCssClass = currentStepObj.tooltipClass;
    } else {
      tooltipCssClass = this._options.tooltipClass;
    }

    tooltipLayer.className = ('guidejs-tooltip ' + tooltipCssClass).replace(/^\s+|\s+$/g, '');
    tooltipLayer.setAttribute('role', 'dialog');

    currentTooltipPosition = this._guideItems[this._currentStep].position;

    if (currentTooltipPosition !== 'floating') {
      currentTooltipPosition = _determineAutoPosition.call(
        this,
        targetElement,
        tooltipLayer,
        currentTooltipPosition,
      );
    }

    let tooltipLayerStyleLeft;
    targetOffset = _getOffset(targetElement);
    tooltipOffset = _getOffset(tooltipLayer);
    windowSize = _getWinSize();

    _addClass(tooltipLayer, 'guidejs-' + currentTooltipPosition);

    switch (currentTooltipPosition) {
      case 'top-right-aligned':
        arrowLayer.className = 'guidejs-arrow bottom-right';

        var tooltipLayerStyleRight = 0;
        _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer);
        tooltipLayer.style.bottom = targetOffset.height + 20 + 'px';
        break;

      case 'top-middle-aligned':
        arrowLayer.className = 'guidejs-arrow bottom-middle';

        var tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2;

        if (_checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) {
          tooltipLayer.style.right = null;
          _checkRight(
            targetOffset,
            tooltipLayerStyleLeftRight,
            tooltipOffset,
            windowSize,
            tooltipLayer,
          );
        }
        tooltipLayer.style.bottom = targetOffset.height + 20 + 'px';
        break;

      case 'top-left-aligned':
      case 'top':
        arrowLayer.className = 'guidejs-arrow bottom';

        _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer);
        tooltipLayer.style.bottom = targetOffset.height + 20 + 'px';
        break;
      case 'right':
        tooltipLayer.style.left = targetOffset.width + 20 + 'px';
        if (targetOffset.top + tooltipOffset.height > windowSize.height) {
          arrowLayer.className = 'guidejs-arrow left-bottom';
          tooltipLayer.style.top = '-' + (tooltipOffset.height - targetOffset.height - 20) + 'px';
        } else {
          arrowLayer.className = 'guidejs-arrow left';
        }
        break;
      case 'left':
        if (targetOffset.top + tooltipOffset.height > windowSize.height) {
          tooltipLayer.style.top = '-' + (tooltipOffset.height - targetOffset.height - 20) + 'px';
          arrowLayer.className = 'guidejs-arrow right-bottom';
        } else {
          arrowLayer.className = 'guidejs-arrow right';
        }
        tooltipLayer.style.right = targetOffset.width + 20 + 'px';

        break;
      case 'floating':
        arrowLayer.style.display = 'none';
        tooltipLayer.style.left = '50%';
        tooltipLayer.style.top = '50%';
        tooltipLayer.style.marginLeft = '-' + tooltipOffset.width / 2 + 'px';
        tooltipLayer.style.marginTop = '-' + tooltipOffset.height / 2 + 'px';

        if (typeof helperNumberLayer !== 'undefined' && helperNumberLayer !== null) {
          helperNumberLayer.style.left = '-' + (tooltipOffset.width / 2 + 18) + 'px';
          helperNumberLayer.style.top = '-' + (tooltipOffset.height / 2 + 18) + 'px';
        }

        break;
      case 'bottom-right-aligned':
        arrowLayer.className = 'guidejs-arrow top-right';

        tooltipLayerStyleRight = 0;
        _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer);
        tooltipLayer.style.top = targetOffset.height + 20 + 'px';
        break;

      case 'bottom-middle-aligned':
        arrowLayer.className = 'guidejs-arrow top-middle';

        tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2;

        if (_checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) {
          tooltipLayer.style.right = null;
          _checkRight(
            targetOffset,
            tooltipLayerStyleLeftRight,
            tooltipOffset,
            windowSize,
            tooltipLayer,
          );
        }
        tooltipLayer.style.top = targetOffset.height + 20 + 'px';
        break;
      default:
        arrowLayer.className = 'guidejs-arrow top';

        tooltipLayerStyleLeft = 0;
        _checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer);
        tooltipLayer.style.top = targetOffset.height + 20 + 'px';
    }
  }

  function _checkRight(
    targetOffset,
    tooltipLayerStyleLeft,
    tooltipOffset,
    windowSize,
    tooltipLayer,
  ) {
    if (targetOffset.left + tooltipLayerStyleLeft + tooltipOffset.width > windowSize.width) {
      tooltipLayer.style.left = windowSize.width - tooltipOffset.width - targetOffset.left + 'px';
      return false;
    }
    tooltipLayer.style.left = tooltipLayerStyleLeft + 'px';
    return true;
  }

  function _checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer) {
    if (targetOffset.left + targetOffset.width - tooltipLayerStyleRight - tooltipOffset.width < 0) {
      tooltipLayer.style.left = -targetOffset.left + 'px';
      return false;
    }
    tooltipLayer.style.right = tooltipLayerStyleRight + 'px';
    return true;
  }

  function _determineAutoPosition(targetElement, tooltipLayer, desiredTooltipPosition) {
    const possiblePositions = this._options.positionPrecedence.slice();

    const windowSize = _getWinSize();
    const tooltipHeight = _getOffset(tooltipLayer).height + 10;
    const tooltipWidth = _getOffset(tooltipLayer).width + 20;
    const targetElementRect = targetElement.getBoundingClientRect();
    let calculatedPosition = 'floating';

    if (targetElementRect.bottom + tooltipHeight > windowSize.height) {
      _removeEntry(possiblePositions, 'bottom');
    }

    if (targetElementRect.top - tooltipHeight < 0) {
      _removeEntry(possiblePositions, 'top');
    }

    if (targetElementRect.right + tooltipWidth > windowSize.width) {
      _removeEntry(possiblePositions, 'right');
    }

    if (targetElementRect.left - tooltipWidth < 0) {
      _removeEntry(possiblePositions, 'left');
    }

    const desiredAlignment = (function (pos) {
      const hyphenIndex = pos.indexOf('-');
      if (hyphenIndex !== -1) {
        return pos.substr(hyphenIndex);
      }
      return '';
    })(desiredTooltipPosition || '');

    if (desiredTooltipPosition) {
      desiredTooltipPosition = desiredTooltipPosition.split('-')[0];
    }

    if (possiblePositions.length) {
      if (
        desiredTooltipPosition !== 'auto' &&
        possiblePositions.indexOf(desiredTooltipPosition) > -1
      ) {
        calculatedPosition = desiredTooltipPosition;
      } else {
        calculatedPosition = possiblePositions[0];
      }
    }
    if (['top', 'bottom'].indexOf(calculatedPosition) !== -1) {
      calculatedPosition += _determineAutoAlignment(
        targetElementRect.left,
        tooltipWidth,
        windowSize,
        desiredAlignment,
      );
    }

    return calculatedPosition;
  }

  function _determineAutoAlignment(offsetLeft, tooltipWidth, windowSize, desiredAlignment) {
    let halfTooltipWidth = tooltipWidth / 2,
      winWidth = Math.min(windowSize.width, window.screen.width),
      possibleAlignments = ['-left-aligned', '-middle-aligned', '-right-aligned'],
      calculatedAlignment = '';

    if (winWidth - offsetLeft < tooltipWidth) {
      _removeEntry(possibleAlignments, '-left-aligned');
    }

    if (offsetLeft < halfTooltipWidth || winWidth - offsetLeft < halfTooltipWidth) {
      _removeEntry(possibleAlignments, '-middle-aligned');
    }

    if (offsetLeft < tooltipWidth) {
      _removeEntry(possibleAlignments, '-right-aligned');
    }

    if (possibleAlignments.length) {
      if (possibleAlignments.indexOf(desiredAlignment) !== -1) {
        calculatedAlignment = desiredAlignment;
      } else {
        calculatedAlignment = possibleAlignments[0];
      }
    } else {
      calculatedAlignment = '-middle-aligned';
    }

    return calculatedAlignment;
  }

  function _removeEntry(stringArray, stringToRemove) {
    if (stringArray.indexOf(stringToRemove) > -1) {
      stringArray.splice(stringArray.indexOf(stringToRemove), 1);
    }
  }

  function _setHelperLayerPosition(helperLayer) {
    if (helperLayer) {
      if (!this._guideItems[this._currentStep]) return;

      let currentElement = this._guideItems[this._currentStep],
        elementPosition = _getOffset(currentElement.element),
        widthHeightPadding = this._options.helperElementPadding;
      if (_isFixed(currentElement.element)) {
        _addClass(helperLayer, 'guidejs-fixedTooltip');
      } else {
        _removeClass(helperLayer, 'guidejs-fixedTooltip');
      }

      if (currentElement.position === 'floating') {
        widthHeightPadding = 0;
      }
      helperLayer.style.cssText =
        'width: ' +
        (elementPosition.width + widthHeightPadding) +
        'px; ' +
        'height:' +
        (elementPosition.height + widthHeightPadding) +
        'px; ' +
        'top:' +
        (elementPosition.top - widthHeightPadding / 2) +
        'px;' +
        'left: ' +
        (elementPosition.left - widthHeightPadding / 2) +
        'px;';
    }
  }

  function _disableInteraction() {
    let disableInteractionLayer = document.querySelector('.guidejs-disableInteraction');
    if (disableInteractionLayer === null) {
      disableInteractionLayer = document.createElement('div');
      disableInteractionLayer.className = 'guidejs-disableInteraction';
      this._targetElement.appendChild(disableInteractionLayer);
    }
    _setHelperLayerPosition.call(this, disableInteractionLayer);
  }

  function _setAnchorAsButton(anchor) {
    anchor.setAttribute('role', 'button');
    anchor.tabIndex = 0;
  }

  function _showElement(targetElement) {
    if (typeof this._guideChangeCallback !== 'undefined') {
      this._guideChangeCallback.call(this, targetElement.element);
    }

    let self = this,
      oldHelperLayer = document.querySelector('.guidejs-helperLayer'),
      oldReferenceLayer: any = document.querySelector('.guidejs-tooltipReferenceLayer'),
      highlightClass = 'guidejs-helperLayer',
      nextTooltipButton,
      prevTooltipButton,
      skipTooltipButton,
      scrollParent;

    if (typeof targetElement.highlightClass === 'string') {
      highlightClass += ' ' + targetElement.highlightClass;
    }

    if (typeof this._options.highlightClass === 'string') {
      highlightClass += ' ' + this._options.highlightClass;
    }

    if (oldHelperLayer !== null) {
      const oldHelperNumberLayer = oldReferenceLayer.querySelector('.guidejs-helperNumberLayer'),
        oldtooltipLayer = oldReferenceLayer.querySelector('.guidejs-tooltiptext'),
        oldArrowLayer = oldReferenceLayer.querySelector('.guidejs-arrow'),
        oldtooltipContainer = oldReferenceLayer.querySelector('.guidejs-tooltip');

      skipTooltipButton = oldReferenceLayer.querySelector('.guidejs-skipbutton');
      prevTooltipButton = oldReferenceLayer.querySelector('.guidejs-prevbutton');
      nextTooltipButton = oldReferenceLayer.querySelector('.guidejs-nextbutton');

      oldHelperLayer.className = highlightClass;

      oldtooltipContainer.style.opacity = 0;
      oldtooltipContainer.style.display = 'none';

      if (oldHelperNumberLayer !== null) {
        const lastGuideItem =
          this._guideItems[targetElement.step - 2 >= 0 ? targetElement.step - 2 : 0];

        if (
          (lastGuideItem !== null &&
            this._direction === 'forward' &&
            lastGuideItem.position === 'floating') ||
          (this._direction === 'backward' && targetElement.position === 'floating')
        ) {
          oldHelperNumberLayer.style.opacity = 0;
        }
      }

      scrollParent = _getScrollParent(targetElement.element);

      if (scrollParent !== document.body) {
        _scrollParentToElement(scrollParent, targetElement.element);
      }

      _setHelperLayerPosition.call(self, oldHelperLayer);
      _setHelperLayerPosition.call(self, oldReferenceLayer);

      const fixParents = document.querySelectorAll('.guidejs-fixParent');
      _forEach(fixParents, function (parent) {
        _removeClass(parent, /guidejs-fixParent/g);
      });

      _removeShowElement();

      if (self._lastShowElementTimer) {
        window.clearTimeout(self._lastShowElementTimer);
      }

      self._lastShowElementTimer = window.setTimeout(function () {
        if (oldHelperNumberLayer !== null) {
          oldHelperNumberLayer.innerHTML = targetElement.step;
        }
        oldtooltipLayer.innerHTML = targetElement.guide;
        oldtooltipContainer.style.display = 'block';
        _placeTooltip.call(
          self,
          targetElement.element,
          oldtooltipContainer,
          oldArrowLayer,
          oldHelperNumberLayer,
        );

        if (self._options.showBullets) {
          oldReferenceLayer.querySelector('.guidejs-bullets li > a.active').className = '';
          oldReferenceLayer.querySelector(
            '.guidejs-bullets li > a[data-stepnumber="' + targetElement.step + '"]',
          ).className = 'active';
        }
        oldReferenceLayer.querySelector('.guidejs-progress .guidejs-progressbar').style.cssText =
          'width:' + _getProgress.call(self) + '%;';
        oldReferenceLayer
          .querySelector('.guidejs-progress .guidejs-progressbar')
          .setAttribute('aria-valuenow', _getProgress.call(self));

        (oldtooltipContainer as any).style.opacity = 1;
        if (oldHelperNumberLayer) (oldHelperNumberLayer as any).style.opacity = 1;

        if (
          typeof skipTooltipButton !== 'undefined' &&
          skipTooltipButton !== null &&
          /guidejs-donebutton/gi.test(skipTooltipButton.className)
        ) {
          skipTooltipButton.focus();
        } else if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
          nextTooltipButton.focus();
        }
        _scrollTo.call(self, targetElement.scrollTo, targetElement, oldtooltipLayer);
      }, 350);
    } else {
      const helperLayer = document.createElement('div'),
        referenceLayer = document.createElement('div'),
        arrowLayer = document.createElement('div'),
        tooltipLayer = document.createElement('div'),
        tooltipTextLayer = document.createElement('div'),
        bulletsLayer = document.createElement('div'),
        progressLayer = document.createElement('div'),
        buttonsLayer = document.createElement('div');

      helperLayer.className = highlightClass;
      referenceLayer.className = 'guidejs-tooltipReferenceLayer';
      scrollParent = _getScrollParent(targetElement.element);

      if (scrollParent !== document.body) {
        _scrollParentToElement(scrollParent, targetElement.element);
      }
      _setHelperLayerPosition.call(self, helperLayer);
      _setHelperLayerPosition.call(self, referenceLayer);
      this._targetElement.appendChild(helperLayer);
      this._targetElement.appendChild(referenceLayer);

      arrowLayer.className = 'guidejs-arrow';

      tooltipTextLayer.className = 'guidejs-tooltiptext';
      tooltipTextLayer.innerHTML = targetElement.guide;

      bulletsLayer.className = 'guidejs-bullets';

      if (this._options.showBullets === false) {
        bulletsLayer.style.display = 'none';
      }

      const ulContainer = document.createElement('ul');
      ulContainer.setAttribute('role', 'tablist');

      const anchorClick = function () {
        self.goToStep(this.getAttribute('data-stepnumber'));
      };

      _forEach(this._guideItems, function (item, i) {
        const innerLi = document.createElement('li');
        const anchorLink = document.createElement('a');

        innerLi.setAttribute('role', 'presentation');
        anchorLink.setAttribute('role', 'tab');

        anchorLink.onclick = anchorClick;

        if (i === targetElement.step - 1) {
          anchorLink.className = 'active';
        }

        _setAnchorAsButton(anchorLink);
        anchorLink.innerHTML = '&nbsp;';
        anchorLink.setAttribute('data-stepnumber', item.step);

        innerLi.appendChild(anchorLink);
        ulContainer.appendChild(innerLi);
      });

      bulletsLayer.appendChild(ulContainer);

      progressLayer.className = 'guidejs-progress';

      if (this._options.showProgress === false) {
        progressLayer.style.display = 'none';
      }
      const progressBar = document.createElement('div');
      progressBar.className = 'guidejs-progressbar';
      progressBar.setAttribute('role', 'progress');
      progressBar.setAttribute('aria-valuemin', '0');
      progressBar.setAttribute('aria-valuemax', '100');
      progressBar.setAttribute('aria-valuenow', _getProgress.call(this));
      progressBar.style.cssText = 'width:' + _getProgress.call(this) + '%;';

      progressLayer.appendChild(progressBar);

      buttonsLayer.className = 'guidejs-tooltipbuttons';
      if (this._options.showButtons === false) {
        buttonsLayer.style.display = 'none';
      }

      tooltipLayer.className = 'guidejs-tooltip';
      tooltipLayer.appendChild(tooltipTextLayer);
      tooltipLayer.appendChild(bulletsLayer);
      tooltipLayer.appendChild(progressLayer);

      const helperNumberLayer = document.createElement('span');
      if (this._options.showStepNumbers === true) {
        helperNumberLayer.className = 'guidejs-helperNumberLayer';
        helperNumberLayer.innerHTML = targetElement.step;
        referenceLayer.appendChild(helperNumberLayer);
      }

      tooltipLayer.appendChild(arrowLayer);
      referenceLayer.appendChild(tooltipLayer);

      nextTooltipButton = document.createElement('a');

      nextTooltipButton.onclick = function () {
        if (self._guideItems.length - 1 !== self._currentStep) {
          _nextStep.call(self);
        }
      };

      _setAnchorAsButton(nextTooltipButton);
      nextTooltipButton.innerHTML = this._options.nextLabel;

      prevTooltipButton = document.createElement('a');

      prevTooltipButton.onclick = function () {
        if (self._currentStep !== 0) {
          _previousStep.call(self);
        }
      };

      _setAnchorAsButton(prevTooltipButton);
      prevTooltipButton.innerHTML = this._options.prevLabel;

      skipTooltipButton = document.createElement('a');
      skipTooltipButton.className = this._options.buttonClass + ' guidejs-skipbutton ';
      _setAnchorAsButton(skipTooltipButton);
      skipTooltipButton.innerHTML = this._options.skipLabel;

      skipTooltipButton.onclick = function () {
        if (
          self._guideItems.length - 1 === self._currentStep &&
          typeof self._guideCompleteCallback === 'function'
        ) {
          self._guideCompleteCallback.call(self);
        }

        if (
          self._guideItems.length - 1 !== self._currentStep &&
          typeof self._guideExitCallback === 'function'
        ) {
          self._guideExitCallback.call(self);
        }

        if (typeof self._guideSkipCallback === 'function') {
          self._guideSkipCallback.call(self);
        }

        _exitGuide.call(self, self._targetElement);
      };

      buttonsLayer.appendChild(skipTooltipButton);

      if (this._guideItems.length > 1) {
        buttonsLayer.appendChild(prevTooltipButton);
        buttonsLayer.appendChild(nextTooltipButton);
      }

      tooltipLayer.appendChild(buttonsLayer);

      _placeTooltip.call(self, targetElement.element, tooltipLayer, arrowLayer, helperNumberLayer);

      _scrollTo.call(this, targetElement.scrollTo, targetElement, tooltipLayer);
    }

    const disableInteractionLayer = self._targetElement.querySelector(
      '.guidejs-disableInteraction',
    );
    if (disableInteractionLayer) {
      disableInteractionLayer.parentNode.removeChild(disableInteractionLayer);
    }

    if (targetElement.disableInteraction) {
      _disableInteraction.call(self);
    }
    if (this._currentStep === 0 && this._guideItems.length > 1) {
      if (typeof skipTooltipButton !== 'undefined' && skipTooltipButton !== null) {
        skipTooltipButton.className = this._options.buttonClass + ' guidejs-skipbutton';
      }
      if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
        nextTooltipButton.className = this._options.buttonClass + ' guidejs-nextbutton';
      }

      if (this._options.hidePrev === true) {
        if (typeof prevTooltipButton !== 'undefined' && prevTooltipButton !== null) {
          prevTooltipButton.className =
            this._options.buttonClass + ' guidejs-prevbutton guidejs-hidden';
        }
        if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
          _addClass(nextTooltipButton, 'guidejs-fullbutton');
        }
      } else {
        if (typeof prevTooltipButton !== 'undefined' && prevTooltipButton !== null) {
          prevTooltipButton.className =
            this._options.buttonClass + ' guidejs-prevbutton guidejs-disabled';
        }
      }

      if (typeof skipTooltipButton !== 'undefined' && skipTooltipButton !== null) {
        skipTooltipButton.innerHTML = this._options.skipLabel;
      }
    } else if (this._guideItems.length - 1 === this._currentStep || this._guideItems.length === 1) {
      if (typeof skipTooltipButton !== 'undefined' && skipTooltipButton !== null) {
        skipTooltipButton.innerHTML = this._options.doneLabel;
        _addClass(skipTooltipButton, 'guidejs-donebutton');
      }
      if (typeof prevTooltipButton !== 'undefined' && prevTooltipButton !== null) {
        prevTooltipButton.className = this._options.buttonClass + ' guidejs-prevbutton';
      }

      if (this._options.hideNext === true) {
        if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
          nextTooltipButton.className =
            this._options.buttonClass + ' guidejs-nextbutton guidejs-hidden';
        }
        if (typeof prevTooltipButton !== 'undefined' && prevTooltipButton !== null) {
          _addClass(prevTooltipButton, 'guidejs-fullbutton');
        }
      } else {
        if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
          nextTooltipButton.className =
            this._options.buttonClass + ' guidejs-nextbutton guidejs-disabled';
        }
      }
    } else {
      if (typeof skipTooltipButton !== 'undefined' && skipTooltipButton !== null) {
        skipTooltipButton.className = this._options.buttonClass + ' guidejs-skipbutton';
      }
      if (typeof prevTooltipButton !== 'undefined' && prevTooltipButton !== null) {
        prevTooltipButton.className = this._options.buttonClass + ' guidejs-prevbutton';
      }
      if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
        nextTooltipButton.className = this._options.buttonClass + ' guidejs-nextbutton';
      }
      if (typeof skipTooltipButton !== 'undefined' && skipTooltipButton !== null) {
        skipTooltipButton.innerHTML = this._options.skipLabel;
      }
    }

    prevTooltipButton.setAttribute('role', 'button');
    nextTooltipButton.setAttribute('role', 'button');
    skipTooltipButton.setAttribute('role', 'button');
    if (typeof nextTooltipButton !== 'undefined' && nextTooltipButton !== null) {
      nextTooltipButton.focus();
    }

    _setShowElement(targetElement);

    if (typeof this._guideAfterChangeCallback !== 'undefined') {
      this._guideAfterChangeCallback.call(this, targetElement.element);
    }
  }

  function _scrollTo(scrollTo, targetElement, tooltipLayer) {
    if (scrollTo === 'off') return;
    let rect;

    if (!this._options.scrollToElement) return;

    if (scrollTo === 'tooltip') {
      rect = tooltipLayer.getBoundingClientRect();
    } else {
      rect = targetElement.element.getBoundingClientRect();
    }

    if (!_elementInViewport(targetElement.element)) {
      const winHeight = _getWinSize().height;
      const top = rect.bottom - (rect.bottom - rect.top);

      if (top < 0 || targetElement.element.clientHeight > winHeight) {
        window.scrollBy(
          0,
          rect.top - (winHeight / 2 - rect.height / 2) - this._options.scrollPadding,
        );
      } else {
        window.scrollBy(
          0,
          rect.top - (winHeight / 2 - rect.height / 2) + this._options.scrollPadding,
        );
      }
    }
  }

  function _removeShowElement() {
    const elms = document.querySelectorAll('.guidejs-showElement');

    _forEach(elms, function (elm) {
      _removeClass(elm, /guidejs-[a-zA-Z]+/g);
    });
  }

  function _setShowElement(targetElement) {
    let parentElm;
    if (targetElement.element instanceof SVGElement) {
      parentElm = targetElement.element.parentNode;

      while (targetElement.element.parentNode !== null) {
        if (!parentElm.tagName || parentElm.tagName.toLowerCase() === 'body') break;

        if (parentElm.tagName.toLowerCase() === 'svg') {
          _addClass(parentElm, 'guidejs-showElement guidejs-relativePosition');
        }

        parentElm = parentElm.parentNode;
      }
    }

    _addClass(targetElement.element, 'guidejs-showElement');

    const currentElementPosition = _getPropValue(targetElement.element, 'position');
    if (
      currentElementPosition !== 'absolute' &&
      currentElementPosition !== 'relative' &&
      currentElementPosition !== 'fixed'
    ) {
      _addClass(targetElement.element, 'guidejs-relativePosition');
    }

    parentElm = targetElement.element.parentNode;
    while (parentElm !== null) {
      if (!parentElm.tagName || parentElm.tagName.toLowerCase() === 'body') break;
      const zIndex = _getPropValue(parentElm, 'z-index');
      const opacity = parseFloat(_getPropValue(parentElm, 'opacity'));
      const transform =
        _getPropValue(parentElm, 'transform') ||
        _getPropValue(parentElm, '-webkit-transform') ||
        _getPropValue(parentElm, '-moz-transform') ||
        _getPropValue(parentElm, '-ms-transform') ||
        _getPropValue(parentElm, '-o-transform');
      if (
        /[0-9]+/.test(zIndex) ||
        opacity < 1 ||
        (transform !== 'none' && transform !== undefined)
      ) {
        _addClass(parentElm, 'guidejs-fixParent');
      }

      parentElm = parentElm.parentNode;
    }
  }

  function _forEach(arr, forEachFnc, completeFnc?) {
    if (arr) {
      for (let i = 0, len = arr.length; i < len; i++) {
        forEachFnc(arr[i], i);
      }
    }

    if (typeof completeFnc === 'function') {
      completeFnc();
    }
  }

  const _stamp = (function () {
    const keys = {};
    return function stamp(obj, key?) {
      key = key || 'guidejs-stamp';
      keys[key] = keys[key] || 0;
      if (obj[key] === undefined) {
        obj[key] = keys[key]++;
      }

      return obj[key];
    };
  })();

  var DOMEvent = (function () {
    function DOMEvent() {
      const events_key = 'guidejs_event';

      this._id = function (_obj, type, listener, context) {
        return type + _stamp(listener) + (context ? '_' + _stamp(context) : '');
      };

      this.on = function (obj, type, listener, context, useCapture) {
        const id = this._id.apply(this, arguments),
          handler = function (e) {
            return listener.call(context || obj, e || window.event);
          };

        if ('addEventListener' in obj) {
          obj.addEventListener(type, handler, useCapture);
        } else if ('attachEvent' in obj) {
          obj.attachEvent('on' + type, handler);
        }

        obj[events_key] = obj[events_key] || {};
        obj[events_key][id] = handler;
      };

      this.off = function (obj, type, _listener, _context, useCapture) {
        const id = this._id.apply(this, arguments),
          handler = obj[events_key] && obj[events_key][id];

        if (!handler) {
          return;
        }

        if ('removeEventListener' in obj) {
          obj.removeEventListener(type, handler, useCapture);
        } else if ('detachEvent' in obj) {
          obj.detachEvent('on' + type, handler);
        }

        obj[events_key][id] = null;
      };
    }

    return new DOMEvent();
  })();

  function _addClass(element, className) {
    if (element instanceof SVGElement) {
      const pre = element.getAttribute('class') || '';

      element.setAttribute('class', pre + ' ' + className);
    } else {
      if (element.classList !== undefined) {
        const classes = className.split(' ');
        _forEach(classes, function (cls) {
          element.classList.add(cls);
        });
      } else if (!element.className.match(className)) {
        element.className += ' ' + className;
      }
    }
  }

  function _removeClass(element, classNameRegex) {
    if (element instanceof SVGElement) {
      const pre = element.getAttribute('class') || '';

      element.setAttribute('class', pre.replace(classNameRegex, '').replace(/^\s+|\s+$/g, ''));
    } else {
      element.className = element.className.replace(classNameRegex, '').replace(/^\s+|\s+$/g, '');
    }
  }

  function _getPropValue(element, propName) {
    let propValue = '';
    if (element.currentStyle) {
      propValue = element.currentStyle[propName];
    } else if (document.defaultView && document.defaultView.getComputedStyle) {
      //Others
      propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName);
    }

    if (propValue && propValue.toLowerCase) {
      return propValue.toLowerCase();
    } else {
      return propValue;
    }
  }

  function _isFixed(element) {
    const p = element.parentNode;

    if (!p || p.nodeName === 'HTML') {
      return false;
    }

    if (_getPropValue(element, 'position') === 'fixed') {
      return true;
    }

    return _isFixed(p);
  }

  function _getWinSize() {
    if (window.innerWidth !== undefined) {
      return { width: window.innerWidth, height: window.innerHeight };
    } else {
      const D = document.documentElement;
      return { width: D.clientWidth, height: D.clientHeight };
    }
  }

  function _elementInViewport(el) {
    const rect = el.getBoundingClientRect();

    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom + 80 <= window.innerHeight &&
      rect.right <= window.innerWidth
    );
  }

  function _addOverlayLayer(targetElm) {
    let overlayLayer = document.createElement('div'),
      styleText = '',
      self = this;

    overlayLayer.className = 'guidejs-overlay';

    if (!targetElm.tagName || targetElm.tagName.toLowerCase() === 'body') {
      styleText += 'top: 0;bottom: 0; left: 0;right: 0;position: fixed;';
      overlayLayer.style.cssText = styleText;
    } else {
      const elementPosition = _getOffset(targetElm);
      if (elementPosition) {
        styleText +=
          'width: ' +
          elementPosition.width +
          'px; height:' +
          elementPosition.height +
          'px; top:' +
          elementPosition.top +
          'px;left: ' +
          elementPosition.left +
          'px;';
        overlayLayer.style.cssText = styleText;
      }
    }

    targetElm.appendChild(overlayLayer);

    overlayLayer.onclick = function () {
      if (self._options.exitOnOverlayClick === true) {
        _exitGuide.call(self, targetElm);
      }
    };

    window.setTimeout(function () {
      styleText += 'opacity: ' + self._options.overlayOpacity.toString() + ';';
      overlayLayer.style.cssText = styleText;
    }, 10);

    return true;
  }

  function _getOffset(element) {
    const body = document.body;
    const docEl = document.documentElement;
    const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
    const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
    const x = element.getBoundingClientRect();
    return {
      top: x.top + scrollTop,
      width: x.width,
      height: x.height,
      left: x.left + scrollLeft,
    };
  }

  function _getScrollParent(element) {
    let style = window.getComputedStyle(element);
    const excludeStaticParent = style.position === 'absolute';
    const overflowRegex = /(auto|scroll)/;

    if (style.position === 'fixed') return document.body;

    for (let parent = element; (parent = parent.parentElement); ) {
      style = window.getComputedStyle(parent);
      if (excludeStaticParent && style.position === 'static') {
        continue;
      }
      if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
    }

    return document.body;
  }

  function _scrollParentToElement(parent, element) {
    parent.scrollTop = element.offsetTop - parent.offsetTop;
  }

  function _getProgress() {
    const currentStep = parseInt(this._currentStep + 1, 10);
    return (currentStep / this._guideItems.length) * 100;
  }

  function _mergeOptions(obj1, obj2) {
    let obj3 = {},
      attrname;
    for (attrname in obj1) {
      obj3[attrname] = obj1[attrname];
    }
    for (attrname in obj2) {
      obj3[attrname] = obj2[attrname];
    }
    return obj3;
  }

  var guideJs: any = function (targetElm) {
    let instance;

    if (typeof targetElm === 'object') {
      instance = new GuideJs(targetElm);
    } else if (typeof targetElm === 'string') {
      const targetElement = document.querySelector(targetElm);

      if (targetElement) {
        instance = new GuideJs(targetElement);
      } else {
        throw new Error('There is no element with given selector.');
      }
    } else {
      instance = new GuideJs(document.body);
    }
    guideJs.instances[_stamp(instance, 'guidejs-instance')] = instance;

    return instance;
  };

  guideJs.instances = {};
  guideJs.fn = GuideJs.prototype = {
    clone: function () {
      return new GuideJs(this);
    },
    setOption: function (option, value) {
      this._options[option] = value;
      return this;
    },
    setOptions: function (options) {
      this._options = _mergeOptions(this._options, options);
      return this;
    },
    start: function (group) {
      _guideForElement.call(this, this._targetElement, group);
      return this;
    },
    goToStep: function (step) {
      _goToStep.call(this, step);
      return this;
    },
    addStep: function (options) {
      if (!this._options.steps) {
        this._options.steps = [];
      }

      this._options.steps.push(options);

      return this;
    },
    addSteps: function (steps) {
      if (!steps.length) return;

      for (let index = 0; index < steps.length; index++) {
        this.addStep(steps[index]);
      }

      return this;
    },
    goToStepNumber: function (step) {
      _goToStepNumber.call(this, step);

      return this;
    },
    nextStep: function () {
      _nextStep.call(this);
      return this;
    },
    previousStep: function () {
      _previousStep.call(this);
      return this;
    },
    exit: function (force) {
      _exitGuide.call(this, this._targetElement, force);
      return this;
    },
    refresh: function () {
      _refresh.call(this);
      return this;
    },
    onbeforechange: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideBeforeChangeCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onbeforechange was not a function');
      }
      return this;
    },
    onchange: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideChangeCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onchange was not a function.');
      }
      return this;
    },
    onafterchange: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideAfterChangeCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onafterchange was not a function');
      }
      return this;
    },
    oncomplete: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideCompleteCallback = providedCallback;
      } else {
        throw new Error('Provided callback for oncomplete was not a function.');
      }
      return this;
    },

    onexit: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideExitCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onexit was not a function.');
      }
      return this;
    },
    onskip: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideSkipCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onskip was not a function.');
      }
      return this;
    },
    onbeforeexit: function (providedCallback) {
      if (typeof providedCallback === 'function') {
        this._guideBeforeExitCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onbeforeexit was not a function.');
      }
      return this;
    },
  };

  return guideJs;
}

export default guideJs();
