. The tour must attach the popover to
+ // modal-content div, NOT the modal-dialog div. But most coders + dialog plugins put the id on the modal-dialog div.
+ // So for display, we must adjust the step element to point at modal-content under the modal-dialog div. However if we change the step.element
+ // permanently to the modal-content (by changing tour._options.steps), this won't work if the step is reshown (plugin destroys modal, meaning
+ // the element jq object is no longer valid) and could potentially screw up other
+ // parts of a tour that have dialogs. So instead we record the original element used for this step that involves modals, change the step.element
+ // to the modal-content div, then set it back when the step is hidden again.
+ //
+ // This is ONLY done because it's too difficult to unpick all the original tour code that uses step.element directly.
+ this._setStepFlag(this.getCurrentStepIndex(), "elementModalOriginal", step.element);
+
+ // fix the tour element, the actual visible offset comes from modal > modal-dialog > modal-content and step.element is used to calc this offset & size
+ step.element = $(step.element).find(".modal-content:first");
+ }
+
+ $element = $(step.element);
+
+ // is element inside a modal?
+ if($modalObject === null && $element.parents(".modal:first").length)
+ {
+ // find the parent modal div
+ $modalObject = $element.parents(".modal:first");
+ }
+
+ if($modalObject && $modalObject.length > 0)
+ {
+ this._debug("Modal identified, onModalHidden callback available");
+
+ // store the modal element for other calls
+ this._setStepFlag(i, "elementModal", $modalObject)
+
+ // modal in use, add callback
+ var funcModalHelper = function(_this, $_modalObject)
+ {
+ return function ()
+ {
+ _this._debug("Modal close triggered");
+
+ if(typeof(step.onModalHidden) == "function")
+ {
+ // if step onModalHidden returns false, do nothing. returns int, move to the step specified.
+ // Otherwise continue regular next/end functionality
+ var rslt;
+
+ rslt = step.onModalHidden(_this, i);
+
+ if(rslt === false)
+ {
+ _this._debug("onModalHidden returned exactly false, tour step unchanged");
+ return;
+ }
+
+ if(Number.isInteger(rslt))
+ {
+ _this._debug("onModalHidden returned int, tour moving to step " + rslt + 1);
+
+ $_modalObject.off("hidden.bs.modal", funcModalHelper);
+ return _this.goTo(rslt);
+ }
+
+ _this._debug("onModalHidden did not return false or int, continuing tour");
+ }
+
+ $_modalObject.off("hidden.bs.modal", funcModalHelper);
+
+ if (_this._isLast())
+ {
+ return _this.next();
+ }
+ else
+ {
+ return _this.end();
+ }
+ };
+ }(this, $modalObject);
+
+ $modalObject.off("hidden.bs.modal", funcModalHelper).on("hidden.bs.modal", funcModalHelper);
+ }
+
+ // Helper function to actually show the popover using _showPopoverAndOverlay
+ showStepHelper = (function (_this) {
+ return function (e) {
+ if (_this._isOrphan(step)) {
+ if (step.orphan === false)
+ {
+ _this._debug("Skip the orphan step " + (_this._current + 1) + ".\nOrphan option is false and the element " + step.element + " does not exist or is hidden.");
+
+ if(typeof(step.onElementUnavailable) == "function")
+ {
+ _this._debug("Calling onElementUnavailable callback");
+ step.onElementUnavailable(_this, _this._current);
+ }
+
+ if (skipToPrevious) {
+ _this._showPrevStep(true);
+ } else {
+ _this._showNextStep(true);
+ }
+ return;
+ }
+ _this._debug("Show the orphan step " + (_this._current + 1) + ". Orphans option is true.");
+ }
+
+ //console.log(step);
+
+ if (step.autoscroll && !_this._isOrphan(step))
+ {
+ _this._scrollIntoView(i);
+ }
+ else
+ {
+ _this._showPopoverAndOverlay(i);
+ }
+
+ if (step.duration) {
+ return _this.resume();
+ }
+ };
+ })(this);
+
+
+ // delay in millisec specified in step options
+ showDelay = step.delay.show || step.delay;
+ if ({}
+ .toString.call(showDelay) === '[object Number]' && showDelay > 0) {
+ this._debug("Wait " + showDelay + " milliseconds to show the step " + (this._current + 1));
+ window.setTimeout((function (_this) {
+ return function () {
+ return _this._callOnPromiseDone(promise, showStepHelper);
+ };
+ })(this), showDelay);
+ }
+ else
+ {
+ if(step.delayOnElement)
+ {
+ // delay by element existence or max delay (default 2 sec)
+ var $delayElement = null;
+ var delayFunc = null;
+ var _this = this;
+
+ var revalidateDelayElement = function() {
+ if(typeof(step.delayOnElement.delayElement) == "function")
+ return step.delayOnElement.delayElement();
+ else if(step.delayOnElement.delayElement == "element")
+ return $(step.element);
+ else
+ return $(step.delayOnElement.delayElement);
+ };
+ var $delayElement = revalidateDelayElement();
+
+ var delayElementLog = $delayElement.length > 0 ? $delayElement[0].tagName : step.delayOnElement.delayElement;
+
+ var delayMax = (step.delayOnElement.maxDelay ? step.delayOnElement.maxDelay : 2000);
+ this._debug("Wait for element " + delayElementLog + " visible or max " + delayMax + " milliseconds to show the step " + (this._current + 1));
+
+ delayFunc = window.setInterval( function()
+ {
+ _this._debug("Wait for element " + delayElementLog + ": checking...");
+ if($delayElement.length === 0) {
+ $delayElement = revalidateDelayElement();
+ }
+ if($delayElement.is(':visible'))
+ {
+ _this._debug("Wait for element " + delayElementLog + ": found, showing step");
+ window.clearInterval(delayFunc);
+ delayFunc = null;
+ return _this._callOnPromiseDone(promise, showStepHelper);
+ }
+ }, 250);
+
+ // set max delay to greater than default interval check for element appearance
+ if(delayMax < 250)
+ delayMax = 251;
+
+ // Set timer to kill the setInterval call after max delay time expires
+ window.setTimeout( function ()
+ {
+ if(delayFunc)
+ {
+ _this._debug("Wait for element " + delayElementLog + ": max timeout reached without element found");
+ window.clearInterval(delayFunc);
+
+ // showStepHelper will handle broken/missing/invisible element
+ return _this._callOnPromiseDone(promise, showStepHelper);
+ }
+ }, delayMax);
+ }
+ else
+ {
+ // no delay by milliseconds or delay by time
+ this._callOnPromiseDone(promise, showStepHelper);
+ }
+ }
+
+ return promise;
+ };
+
+ Tour.prototype.getCurrentStepIndex = function () {
+ return this._current;
+ };
+
+ Tour.prototype.setCurrentStep = function (value) {
+ if (value != null)
+ {
+ this._current = value;
+ this._setState('current_step', value);
+ }
+ else
+ {
+ this._current = this._getState('current_step');
+ this._current = this._current === null ? 0 : parseInt(this._current, 10);
+ }
+
+ return this;
+ };
+
+
+ Tour.prototype._setState = function (key, value) {
+ var e,
+ keyName;
+ if (this._options.storage) {
+ keyName = this._options.name + "_" + key;
+ try {
+ this._options.storage.setItem(keyName, value);
+ } catch (error) {
+ e = error;
+ if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
+ this._debug('LocalStorage quota exceeded. State storage failed.');
+ }
+ }
+ return this._options.afterSetState(keyName, value);
+ } else {
+ if (this._state == null) {
+ this._state = {};
+ }
+ return this._state[key] = value;
+ }
+ };
+
+ Tour.prototype._removeState = function (key) {
+ var keyName;
+ if (this._options.storage) {
+ keyName = this._options.name + "_" + key;
+ this._options.storage.removeItem(keyName);
+ return this._options.afterRemoveState(keyName);
+ } else {
+ if (this._state != null) {
+ return delete this._state[key];
+ }
+ }
+ };
+
+ Tour.prototype._getState = function (key) {
+ var keyName,
+ value;
+ if (this._options.storage) {
+ keyName = this._options.name + "_" + key;
+ value = this._options.storage.getItem(keyName);
+ } else {
+ if (this._state != null) {
+ value = this._state[key];
+ }
+ }
+ if (value === void 0 || value === 'null') {
+ value = null;
+ }
+ this._options.afterGetState(key, value);
+ return value;
+ };
+
+ Tour.prototype._showNextStep = function (skipOrphan) {
+ var promise,
+ showNextStepHelper,
+ step;
+
+ var skipOrphan = skipOrphan || false;
+
+ showNextStepHelper = (function (_this) {
+ return function (e) {
+ return _this.showStep(_this._current + 1);
+ };
+ })(this);
+
+ promise = void 0;
+
+ step = this.getStep(this._current);
+
+ // only call the onNext handler if this is a click and NOT an orphan skip due to missing element
+ if (skipOrphan === false && step.onNext != null)
+ {
+ rslt = step.onNext(this);
+
+ if(rslt === false)
+ {
+ this._debug("onNext callback returned false, preventing move to next step");
+ return this.showStep(this._current);
+ }
+
+ promise = this._makePromise(rslt);
+ }
+
+ return this._callOnPromiseDone(promise, showNextStepHelper);
+ };
+
+ Tour.prototype._showPrevStep = function (skipOrphan) {
+ var promise,
+ showPrevStepHelper,
+ step;
+
+ var skipOrphan = skipOrphan || false;
+
+ showPrevStepHelper = (function (_this) {
+ return function (e) {
+ return _this.showStep(step.prev);
+ };
+ })(this);
+
+ promise = void 0;
+ step = this.getStep(this._current);
+
+ // only call the onPrev handler if this is a click and NOT an orphan skip due to missing element
+ if (skipOrphan === false && step.onPrev != null)
+ {
+ rslt = step.onPrev(this);
+
+ if(rslt === false)
+ {
+ this._debug("onPrev callback returned false, preventing move to previous step");
+ return this.showStep(this._current);
+ }
+
+ promise = this._makePromise(rslt);
+ }
+
+ return this._callOnPromiseDone(promise, showPrevStepHelper);
+ };
+
+ Tour.prototype._debug = function (text) {
+ if (this._options.debug) {
+ return window.console.log("[ Bootstrap Tour: '" + this._options.name + "' ] " + text);
+ }
+ };
+
+ Tour.prototype._isRedirect = function (host, path, location) {
+ var currentPath;
+ if ((host != null) && host !== '' && (({}
+ .toString.call(host) === '[object RegExp]' && !host.test(location.origin)) || ({}
+ .toString.call(host) === '[object String]' && this._isHostDifferent(host, location)))) {
+ return true;
+ }
+ currentPath = [location.pathname, location.search, location.hash].join('');
+ return (path != null) && path !== '' && (({}
+ .toString.call(path) === '[object RegExp]' && !path.test(currentPath)) || ({}
+ .toString.call(path) === '[object String]' && this._isPathDifferent(path, currentPath)));
+ };
+
+ Tour.prototype._isHostDifferent = function (host, location) {
+ switch ({}
+ .toString.call(host)) {
+ case '[object RegExp]':
+ return !host.test(location.origin);
+ case '[object String]':
+ return this._getProtocol(host) !== this._getProtocol(location.href) || this._getHost(host) !== this._getHost(location.href);
+ default:
+ return true;
+ }
+ };
+
+ Tour.prototype._isPathDifferent = function (path, currentPath) {
+ return this._getPath(path) !== this._getPath(currentPath) || !this._equal(this._getQuery(path), this._getQuery(currentPath)) || !this._equal(this._getHash(path), this._getHash(currentPath));
+ };
+
+ Tour.prototype._isJustPathHashDifferent = function (host, path, location) {
+ var currentPath;
+ if ((host != null) && host !== '') {
+ if (this._isHostDifferent(host, location)) {
+ return false;
+ }
+ }
+ currentPath = [location.pathname, location.search, location.hash].join('');
+ if ({}
+ .toString.call(path) === '[object String]') {
+ return this._getPath(path) === this._getPath(currentPath) && this._equal(this._getQuery(path), this._getQuery(currentPath)) && !this._equal(this._getHash(path), this._getHash(currentPath));
+ }
+ return false;
+ };
+
+ Tour.prototype._redirect = function (step, i, path) {
+ var href;
+ if ($.isFunction(step.redirect)) {
+ return step.redirect.call(this, path);
+ } else {
+ href = {}
+ .toString.call(step.host) === '[object String]' ? "" + step.host + path : path;
+ this._debug("Redirect to " + href);
+ if (this._getState('redirect_to') === ("" + i)) {
+ this._debug("Error redirection loop to " + path);
+ this._removeState('redirect_to');
+ if (step.onRedirectError != null) {
+ return step.onRedirectError(this);
+ }
+ } else {
+ this._setState('redirect_to', "" + i);
+ return document.location.href = href;
+ }
+ }
+ };
+
+ // Tests if the step is orphan
+ // Step can be "orphan" (unattached to any element) if specifically set as such in tour step options, or with an invalid/hidden element
+ Tour.prototype._isOrphan = function (step)
+ {
+ var isOrphan = (step.orphan == true) || (step.element == null) || !$(step.element).length || $(step.element).is(':hidden') && ($(step.element)[0].namespaceURI !== 'http://www.w3.org/2000/svg');
+
+ return isOrphan;
+ };
+
+
+ Tour.prototype._isLast = function () {
+ return this._current < this._options.steps.length - 1;
+ };
+
+ // wraps the calls to show the tour step in a popover and the background overlay.
+ // Note this is ALSO called by scroll event handler. Individual funcs called will determine whether redraws etc are required.
+ Tour.prototype._showPopoverAndOverlay = function (i)
+ {
+ var step;
+
+ if (this.getCurrentStepIndex() !== i || this.ended()) {
+ return;
+ }
+ step = this.getStep(i);
+
+ if (step.backdrop)
+ {
+ this._showOverlayElements(step);
+ }
+
+ this._fixBootstrapSelectPickerZindex(step);
+
+ // Ensure this is called last, to allow preceeding calls to check whether current step popover is already visible.
+ // This is required because this func is called by scroll event. showPopover creates the actual popover with
+ // current step index as a class. Therefore all preceeding funcs can check if they are being called because of a
+ // scroll event (popover class using current step index exists), or because of a step change (class doesn't exist).
+ this._showPopover(step, i);
+
+ if (step.onShown != null)
+ {
+ step.onShown(this);
+ }
+
+ return this;
+ };
+
+ // handles view of popover
+ Tour.prototype._showPopover = function (step, i) {
+ var $element,
+ $tip,
+ isOrphan,
+ options,
+ title,
+ content,
+ percentProgress,
+ modalObject;
+
+ isOrphan = this._isOrphan(step);
+
+
+ // is this step already visible? _showPopover is called by _showPopoverAndOverlay, which is called by window scroll event. This
+ // check prevents the continual flickering of the current tour step - original approach reloaded the popover every scroll event.
+ // Why is this check here and not in _showPopoverAndOverlay? This allows us to selectively redraw elements on scroll.
+ if($(document).find(".popover.tour-" + this._options.name + ".tour-" + this._options.name + "-" + this.getCurrentStepIndex()).length == 0)
+ {
+ // Step not visible, draw first time
+
+ $(".tour-" + this._options.name).remove();
+
+ step.template = this._template(step, i);
+
+ if (isOrphan)
+ {
+ // Note: BS4 popper.js requires additional fiddling to work, see below where popOpts object is created
+ step.element = 'body';
+ step.placement = 'top';
+ }
+
+ $element = $(step.element);
+
+ $element.addClass("tour-" + this._options.name + "-element tour-" + this._options.name + "-" + i + "-element");
+
+
+ if (step.reflex && !isOrphan)
+ {
+ $(step.reflexElement).addClass('tour-step-element-reflex').off((this._reflexEvent(step.reflex)) + ".tour-" + this._options.name).on((this._reflexEvent(step.reflex)) + ".tour-" + this._options.name, (function (_this) {
+ return function () {
+ if (_this._isLast()) {
+ return _this.next();
+ } else {
+ return _this.end();
+ }
+ };
+ })(this));
+ }
+
+
+ title = step.title;
+ content = step.content;
+ percentProgress = parseInt(((i + 1) / this.getStepCount()) * 100);
+
+ if(step.showProgressBar)
+ {
+ if(typeof(step.getProgressBarHTML) == "function")
+ {
+ content = step.getProgressBarHTML(percentProgress) + content;
+ }
+ else
+ {
+ content = '
' + content;
+ }
+ }
+
+ if(step.showProgressText)
+ {
+ if(typeof(step.getProgressTextHTML) == "function")
+ {
+ title += step.getProgressTextHTML(i, percentProgress, this.getStepCount());
+ }
+ else
+ {
+ if(this._options.framework == "bootstrap3")
+ {
+ title += '
' + (i + 1) + '/' + this.getStepCount() + '';
+ }
+ if(this._options.framework == "bootstrap4")
+ {
+ title += '
' + (i + 1) + '/' + this.getStepCount() + '';
+ }
+ }
+ }
+
+ // Tourist v0.10 - split popOpts out of bootstrap popper instantiation due to BS3 / BS4 diverging requirements
+ var popOpts = {
+ placement: step.placement, // When auto is specified, it will dynamically reorient the popover.
+ trigger: 'manual',
+ title: title,
+ content: content,
+ html: true,
+ //sanitize: false, // turns off all bootstrap sanitization of popover content, only use in last resort case - use whiteListAdditions instead!
+ whiteList: this._options.sanitizeWhitelist, // ignored if sanitizeFn is specified
+ sanitizeFn: this._options.sanitizeFunction,
+ animation: step.animation,
+ container: step.container,
+ template: step.template,
+ selector: step.element,
+ //boundary: "viewport", // added for BS4 popper testing. Do not enable, creates visible jump on orphan step scroll to bottom
+ };
+
+ if(this._options.framework == "bootstrap4")
+ {
+ if(isOrphan)
+ {
+ // BS4 uses popper.js, which doesn't have a method of fixing the popper to the center of the viewport without an element. However
+ // BS4 wrapper does some extra funky stuff that means we can't just replace the BS4 popper init code. Instead, fudge the popper
+ // using the offset feature, which params don't seem to be documented properly!
+ popOpts.offset = function(obj)
+ {
+ //console.log(obj);
+
+ var top = Math.max(0, ( ($(window).height() - obj.popper.height) / 2) );
+ var left = Math.max(0, ( ($(window).width() - obj.popper.width) / 2) );
+
+ obj.popper.position="fixed";
+ obj.popper.top = top;
+ obj.popper.bottom = top + obj.popper.height;
+ obj.popper.left = left;
+ obj.popper.right = top + obj.popper.width;
+ return obj;
+ }
+ }
+ else
+ {
+ // BS3 popover accepts jq object or string literal. BS4 popper.js of course doesn't, just to make life extra irritating.
+ popOpts.selector = "#" + step.element[0].id;
+ }
+ }
+
+ // BS4 / popper.js does not accept a jquery object as element. BS3 popover does!
+ if(this._options.framework == "bootstrap4" && isOrphan == false)
+ {
+ popOpts.selector = "#" + step.element[0].id;
+ }
+
+ $element.popover(popOpts);
+ $element.popover('show');
+
+ if(this._options.framework == "bootstrap3")
+ {
+ $tip = $element.data('bs.popover') ? $element.data('bs.popover').tip() : $element.data('popover').tip();
+
+ // For BS3 only. BS4 popper.js reverts this change
+ if ($element.css('position') === 'fixed')
+ {
+ $tip.css('position', 'fixed');
+ }
+
+ if (isOrphan)
+ {
+ this._center($tip);
+ $tip.css('position', 'fixed');
+ }
+ else
+ {
+ this._reposition($tip, step);
+ }
+ }
+
+ if(this._options.framework == "bootstrap4")
+ {
+ $tip = $( ($element.data('bs.popover') ? $element.data('bs.popover').getTipElement() : $element.data('popover').getTipElement() ) );
+ }
+
+ $tip.attr('id', step.id);
+
+ this._debug("Step " + (this._current + 1) + " of " + this._options.steps.length);
+ }
+ else
+ {
+ // Step is already visible, something has requested a redraw. Uncomment code to force redraw on scroll etc
+ //$element = $(step.element);
+ //$tip = $element.data('bs.popover') ? $element.data('bs.popover').tip() : $element.data('popover').tip();
+
+ if (isOrphan)
+ {
+ // unnecessary re-call, when tour step is set up centered it's fixed to the middle.
+ //this._center($tip);
+ }
+ else
+ {
+ // Add some code to shift the popover wherever is required.
+ // NOTE: this approach works for BS3 ONLY. BS4 with popper.js requires manipulation of offset, see popOpts.offset above.
+ //this._reposition($tip, step);
+ }
+ }
+ };
+
+ Tour.prototype._template = function (step, i) {
+ var $navigation,
+ $next,
+ $prev,
+ $resume,
+ $template,
+ template;
+ template = step.template;
+ if (this._isOrphan(step) && {}
+ .toString.call(step.orphan) !== '[object Boolean]') {
+ template = step.orphan;
+ }
+ $template = $.isFunction(template) ? $(template(i, step)) : $(template);
+ $navigation = $template.find('.popover-navigation');
+ $prev = $navigation.find('[data-role="prev"]');
+ $next = $navigation.find('[data-role="next"]');
+ $resume = $navigation.find('[data-role="pause-resume"]');
+ if (this._isOrphan(step)) {
+ $template.addClass('orphan');
+ }
+ $template.addClass("tour-" + this._options.name + " tour-" + this._options.name + "-" + i);
+ if (step.reflex) {
+ $template.addClass("tour-" + this._options.name + "-reflex");
+ }
+ if (step.prev < 0) {
+ $prev.addClass('disabled').prop('disabled', true).prop('tabindex', -1);
+ }
+ if (step.next < 0) {
+ $next.addClass('disabled').prop('disabled', true).prop('tabindex', -1);
+ }
+ if (step.reflexOnly) {
+ $next.hide();
+ }
+ if (!step.duration) {
+ $resume.remove();
+ }
+ return $template.clone().wrap('
').parent().html();
+ };
+
+ Tour.prototype._reflexEvent = function (reflex) {
+ if ({}
+ .toString.call(reflex) === '[object Boolean]') {
+ return 'click';
+ } else {
+ return reflex;
+ }
+ };
+
+ Tour.prototype._reposition = function ($tip, step) {
+ var offsetBottom,
+ offsetHeight,
+ offsetRight,
+ offsetWidth,
+ originalLeft,
+ originalTop,
+ tipOffset;
+ offsetWidth = $tip[0].offsetWidth;
+ offsetHeight = $tip[0].offsetHeight;
+
+ tipOffset = $tip.offset();
+ originalLeft = tipOffset.left;
+ originalTop = tipOffset.top;
+
+ offsetBottom = $(document).height() - tipOffset.top - $tip.outerHeight();
+ if (offsetBottom < 0) {
+ tipOffset.top = tipOffset.top + offsetBottom;
+ }
+
+ offsetRight = $('html').outerWidth() - tipOffset.left - $tip.outerWidth();
+ if (offsetRight < 0) {
+ tipOffset.left = tipOffset.left + offsetRight;
+ }
+ if (tipOffset.top < 0) {
+ tipOffset.top = 0;
+ }
+ if (tipOffset.left < 0) {
+ tipOffset.left = 0;
+ }
+
+ $tip.offset(tipOffset);
+
+ if (step.placement === 'bottom' || step.placement === 'top') {
+ if (originalLeft !== tipOffset.left) {
+ return this._replaceArrow($tip, (tipOffset.left - originalLeft) * 2, offsetWidth, 'left');
+ }
+ } else {
+ if (originalTop !== tipOffset.top) {
+ return this._replaceArrow($tip, (tipOffset.top - originalTop) * 2, offsetHeight, 'top');
+ }
+ }
+ };
+
+ Tour.prototype._center = function ($tip)
+ {
+ $tip.css('top', $(window).outerHeight() / 2 - $tip.outerHeight() / 2);
+
+ return $tip.css('left', $(window).outerWidth() / 2 - $tip.outerWidth() / 2);
+ };
+
+ Tour.prototype._replaceArrow = function ($tip, delta, dimension, position) {
+ return $tip.find('.arrow').css(position, delta ? 50 * (1 - delta / dimension) + '%' : '');
+ };
+
+ Tour.prototype._scrollIntoView = function (i) {
+ var $element,
+ $window,
+ counter,
+ height,
+ offsetTop,
+ scrollTop,
+ step,
+ windowHeight;
+ step = this.getStep(i);
+ $element = $(step.element);
+
+ if(this._isOrphan(step))
+ {
+ // If this is an orphan step, don't auto-scroll. Orphan steps are now css fixed to center of window
+ return this._showPopoverAndOverlay(i);
+ }
+
+ if (!$element.length)
+ {
+ return this._showPopoverAndOverlay(i);
+ }
+
+ $window = $(window);
+ offsetTop = $element.offset().top;
+ height = $element.outerHeight();
+ windowHeight = $window.height();
+ scrollTop = 0;
+ switch (step.placement) {
+ case 'top':
+ scrollTop = Math.max(0, offsetTop - (windowHeight / 2));
+ break;
+ case 'left':
+ case 'right':
+ scrollTop = Math.max(0, (offsetTop + height / 2) - (windowHeight / 2));
+ break;
+ case 'bottom':
+ scrollTop = Math.max(0, (offsetTop + height) - (windowHeight / 2));
+ }
+ this._debug("Scroll into view. ScrollTop: " + scrollTop + ". Element offset: " + offsetTop + ". Window height: " + windowHeight + ".");
+ counter = 0;
+ return $('body, html').stop(true, true).animate({
+ scrollTop: Math.ceil(scrollTop)
+ }, (function (_this) {
+ return function () {
+ if (++counter === 2) {
+ _this._showPopoverAndOverlay(i);
+ return _this._debug("Scroll into view.\nAnimation end element offset: " + ($element.offset().top) + ".\nWindow height: " + ($window.height()) + ".");
+ }
+ };
+ })(this));
+ };
+
+
+ // Note: this method is not required, but remains here in case any future forkers want to reinstate the code that moves a non-orphan popover
+ // when window is scrolled
+ Tour.prototype._onScroll = function (callback, timeout) {
+ return $(window).on("scroll.tour-" + this._options.name, function () {
+ clearTimeout(timeout);
+ return timeout = setTimeout(callback, 100);
+ });
+ };
+
+ Tour.prototype._initMouseNavigation = function () {
+ var _this;
+ _this = this;
+ return $(document).off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='prev']").off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='next']").off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='end']").off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='pause-resume']").on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='next']", (function (_this) {
+ return function (e) {
+ e.preventDefault();
+ return _this.next();
+ };
+ })(this)).on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='prev']", (function (_this) {
+ return function (e) {
+ e.preventDefault();
+ if (_this._current > 0) {
+ return _this.prev();
+ }
+ };
+ })(this)).on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='end']", (function (_this) {
+ return function (e) {
+ e.preventDefault();
+ return _this.end();
+ };
+ })(this)).on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='pause-resume']", function (e) {
+ var $this;
+ e.preventDefault();
+ $this = $(this);
+ $this.text(_this._paused ? $this.data('pause-text') : $this.data('resume-text'));
+ if (_this._paused) {
+ return _this.resume();
+ } else {
+ return _this.pause();
+ }
+ });
+ };
+
+ Tour.prototype._initKeyboardNavigation = function () {
+ if (!this._options.keyboard) {
+ return;
+ }
+ return $(document).on("keyup.tour-" + this._options.name, (function (_this) {
+ return function (e) {
+ if (!e.which) {
+ return;
+ }
+ switch (e.which) {
+ case 39:
+ e.preventDefault();
+ if (_this._isLast()) {
+ return _this.next();
+ } else {
+ return _this.end();
+ }
+ break;
+ case 37:
+ e.preventDefault();
+ if (_this._current > 0) {
+ return _this.prev();
+ }
+ }
+ };
+ })(this));
+ };
+
+ // If param is a promise, returns the promise back to the caller. Otherwise returns null.
+ // Only purpose is to make calls to _callOnPromiseDone() simple - first param of _callOnPromiseDone()
+ // accepts either null or a promise to smart call either promise or straight callback. This
+ // pair of funcs therefore allows easy integration of user code to return callbacks or promises
+ Tour.prototype._makePromise = function (possiblePromise)
+ {
+ if (possiblePromise && $.isFunction(possiblePromise.then))
+ {
+ return possiblePromise;
+ }
+ else
+ {
+ return null;
+ }
+ };
+
+ // Creates a promise wrapping the callback if valid promise is provided as first arg. If
+ // first arg is not a promise, simply uses direct function call of callback.
+ Tour.prototype._callOnPromiseDone = function (promise, callback, arg)
+ {
+ if (promise)
+ {
+ return promise.then(
+ (function (_this)
+ {
+ return function (e)
+ {
+ return callback.call(_this, arg);
+ };
+ }
+ )(this)
+ );
+ }
+ else
+ {
+ return callback.call(this, arg);
+ }
+ };
+
+ // Bootstrap Select custom draws the drop down, force the Z index between Tour overlay and popoper
+ Tour.prototype._fixBootstrapSelectPickerZindex = function(step)
+ {
+ if(this._isOrphan(step))
+ {
+ // If it's an orphan step, it can't be a selectpicker element
+ return;
+ }
+
+ // is the current step already visible?
+ if($(document).find(".popover.tour-" + this._options.name + ".tour-" + this._options.name + "-" + this.getCurrentStepIndex()).length != 0)
+ {
+ // don't waste time redoing the fix
+ return;
+ }
+
+ var $selectpicker;
+ // is this element or child of this element a selectpicker
+ if($(step.element)[0].tagName.toLowerCase() == "select")
+ {
+ $selectpicker = $(step.element);
+ }
+ else
+ {
+ $selectpicker = $(step.element).find("select:first");
+ }
+
+ // is this selectpicker a bootstrap-select: https://github.com/snapappointments/bootstrap-select/
+ if($selectpicker.length > 0 && $selectpicker.parent().hasClass("bootstrap-select"))
+ {
+ this._debug("Fixing Bootstrap SelectPicker");
+ // set zindex to open dropdown over background element
+ $selectpicker.parent().css("z-index", "1101");
+
+ // store the element for other calls. Mainly for when step is hidden, selectpicker must be unfixed / z index reverted to avoid visual issues.
+ // storing element means we don't need to find it again later
+ this._setStepFlag(this.getCurrentStepIndex(), "elementBootstrapSelectpicker", $selectpicker);
+ }
+ }
+
+ // Revert the Z index between Tour overlay and popoper
+ Tour.prototype._unfixBootstrapSelectPickerZindex = function(step)
+ {
+ var $selectpicker = this._getStepFlag(this.getCurrentStepIndex(), "elementBootstrapSelectpicker");
+ if($selectpicker)
+ {
+ this._debug("Unfixing Bootstrap SelectPicker");
+ // set zindex to open dropdown over background element
+ $selectpicker.parent().css("z-index", "auto");
+ }
+ }
+
+ // Shows the preventInteraction div, and the background divs
+ Tour.prototype._showOverlayElements = function (step) {
+ var elementData,
+ isRedraw;
+
+ // check if the popover for the current step already exists (is this a redraw)
+ if($(document).find(".popover.tour-" + this._options.name + ".tour-" + this._options.name + "-" + this.getCurrentStepIndex()).length == 0)
+ {
+ // not a redraw - could be a new step (i.e.: moving to the next step), or could be the result of a window resize event, which destroys the
+ // old popover
+ isRedraw = false;
+ }
+ else
+ {
+ // Yes. Likely this is because of a window scroll event
+ isRedraw = true;
+
+ return;
+ }
+
+ if(step.preventInteraction && !isRedraw)
+ {
+ $(step.backdropContainer).append("
");
+ $("#tourPrevent").width($(step.element).outerWidth());
+ $("#tourPrevent").height($(step.element).outerHeight());
+ $("#tourPrevent").offset($(step.element).offset());
+ }
+
+ docHeight = $(document).height();
+ docWidth = $(document).width();
+
+ if ($(step.element).length === 0 || this._isOrphan(step))
+ {
+ var $backdrop = $('
');
+ $backdrop.offset({top: 0, left: 0});
+ $backdrop.width(docWidth);
+ $backdrop.height(docHeight);
+ $("body").append($backdrop);
+ }
+ else
+ {
+ elementData = {
+ width: $(step.element).innerWidth(),
+ height: $(step.element).innerHeight(),
+ offset: $(step.element).offset()
+ };
+
+ if (step.backdropPadding)
+ {
+ elementData = this._applyBackdropPadding(step.backdropPadding, elementData);
+ }
+
+ var $backdropTop = $('
');
+ $backdropTop.offset({top: 0, left: 0});
+ $backdropTop.width(docWidth);
+ $backdropTop.height(elementData.offset.top);
+
+ var $backdropLeft = $('
');
+ $backdropLeft.width(elementData.offset.left);
+ $backdropLeft.height(elementData.height);
+ $backdropLeft.offset({top: elementData.offset.top, left: 0});
+
+ var $backdropRight = $('
');
+ $backdropRight.width(docWidth - (elementData.width + elementData.offset.left));
+ $backdropRight.height(elementData.height);
+ $backdropRight.offset({top: elementData.offset.top, left: elementData.offset.left + elementData.width});
+
+ var $backdropBottom = $('
');
+ $backdropBottom.width(docWidth);
+ $backdropBottom.height(docHeight - elementData.offset.top - elementData.height);
+ $backdropBottom.offset({top: elementData.offset.top + elementData.height, left: 0});
+
+ $(step.backdropContainer).append($backdropTop);
+ $(step.backdropContainer).append($backdropLeft);
+ $(step.backdropContainer).append($backdropRight);
+ $(step.backdropContainer).append($backdropBottom);
+ }
+ };
+
+ Tour.prototype._hideOverlayElement = function (step)
+ {
+ // remove any previous interaction overlay
+ if($("#tourPrevent").length)
+ {
+ $("#tourPrevent").remove();
+ }
+
+ $(".tour-backdrop").remove();
+ };
+
+ Tour.prototype._applyBackdropPadding = function (padding, data)
+ {
+ if (typeof padding === 'object') {
+ if (padding.top == null) {
+ padding.top = 0;
+ }
+ if (padding.right == null) {
+ padding.right = 0;
+ }
+ if (padding.bottom == null) {
+ padding.bottom = 0;
+ }
+ if (padding.left == null) {
+ padding.left = 0;
+ }
+ data.offset.top = data.offset.top - padding.top;
+ data.offset.left = data.offset.left - padding.left;
+ data.width = data.width + padding.left + padding.right;
+ data.height = data.height + padding.top + padding.bottom;
+ } else {
+ data.offset.top = data.offset.top - padding;
+ data.offset.left = data.offset.left - padding;
+ data.width = data.width + (padding * 2);
+ data.height = data.height + (padding * 2);
+ }
+ return data;
+ };
+
+ Tour.prototype._clearTimer = function () {
+ window.clearTimeout(this._timer);
+ this._timer = null;
+ return this._duration = null;
+ };
+
+
+ // =============================================================================================================================
+
+ Tour.prototype._getProtocol = function (url) {
+ url = url.split('://');
+ if (url.length > 1) {
+ return url[0];
+ } else {
+ return 'http';
+ }
+ };
+
+ Tour.prototype._getHost = function (url) {
+ url = url.split('//');
+ url = url.length > 1 ? url[1] : url[0];
+ return url.split('/')[0];
+ };
+
+ Tour.prototype._getPath = function (path) {
+ return path.replace(/\/?$/, '').split('?')[0].split('#')[0];
+ };
+
+ Tour.prototype._getQuery = function (path) {
+ return this._getParams(path, '?');
+ };
+
+ Tour.prototype._getHash = function (path) {
+ return this._getParams(path, '#');
+ };
+
+ Tour.prototype._getParams = function (path, start) {
+ var j,
+ len,
+ param,
+ params,
+ paramsObject;
+ params = path.split(start);
+ if (params.length === 1) {
+ return {};
+ }
+ params = params[1].split('&');
+ paramsObject = {};
+ for (j = 0, len = params.length; j < len; j++) {
+ param = params[j];
+ param = param.split('=');
+ paramsObject[param[0]] = param[1] || '';
+ }
+ return paramsObject;
+ };
+
+ Tour.prototype._equal = function (obj1, obj2) {
+ var j,
+ k,
+ len,
+ obj1Keys,
+ obj2Keys,
+ v;
+ if ({}
+ .toString.call(obj1) === '[object Object]' && {}
+ .toString.call(obj2) === '[object Object]') {
+ obj1Keys = Object.keys(obj1);
+ obj2Keys = Object.keys(obj2);
+ if (obj1Keys.length !== obj2Keys.length) {
+ return false;
+ }
+ for (k in obj1) {
+ v = obj1[k];
+ if (!this._equal(obj2[k], v)) {
+ return false;
+ }
+ }
+ return true;
+ } else if ({}
+ .toString.call(obj1) === '[object Array]' && {}
+ .toString.call(obj2) === '[object Array]') {
+ if (obj1.length !== obj2.length) {
+ return false;
+ }
+ for (k = j = 0, len = obj1.length; j < len; k = ++j) {
+ v = obj1[k];
+ if (!this._equal(v, obj2[k])) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return obj1 === obj2;
+ }
+ };
+
+ return Tour;
+
+ })();
+ return Tour;
+});
diff --git a/package-lock.json b/package-lock.json
index a100c3d7..8d4f4ae8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -226,6 +226,11 @@
"integrity": "sha1-L/nQfJs+2ynW0oD+B1KDZefs05I=",
"dev": true
},
+ "bootstrap-tourist": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/bootstrap-tourist/-/bootstrap-tourist-0.2.1.tgz",
+ "integrity": "sha512-qIJBrS64Lj/4Cz/yWaRCrKT82R0hLkH5W3T8Kyyu1eRlEUHt4DygiJqH4YDuwc2CsTZagpikxr7sUXu1X1Cn6w=="
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1083,8 +1088,7 @@
"jquery": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
- "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==",
- "dev": true
+ "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
},
"jsbn": {
"version": "0.1.1",
@@ -2132,12 +2136,6 @@
"browser-process-hrtime": "^0.1.2"
}
},
- "wavedrom-cli": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/wavedrom-cli/-/wavedrom-cli-0.5.0.tgz",
- "integrity": "sha1-h+lb+naRpbaTmPJzL13QycKkiCE=",
- "dev": true
- },
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
diff --git a/package.json b/package.json
index 1afcecc6..52a2aa77 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,10 @@
"url": "git+https://github.com/sehugg/8bitworkshop.git"
},
"license": "GPL-3.0",
- "dependencies": {},
+ "dependencies": {
+ "bootstrap-tourist": "^0.2.1",
+ "jquery": "^3.4.1"
+ },
"devDependencies": {
"@types/bootbox": "^4.4.36",
"@types/bootstrap": "^3.3.42",
@@ -17,7 +20,6 @@
"btoa": "^1.2.x",
"chromedriver": "^76.0.1",
"clipboard": "^2.0.4",
- "jquery": "^3.4.1",
"jsdom": "^12.2.0",
"lzg": "^1.0.x",
"mocha": "^5.2.x",
@@ -25,8 +27,7 @@
"octokat": "^0.10.0",
"pngjs": "^3.3.3",
"typescript": "^3.6.2",
- "typescript-formatter": "^7.2.2",
- "wavedrom-cli": "^0.5.x"
+ "typescript-formatter": "^7.2.2"
},
"main": "main.js",
"directories": {