Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| /* ======================================================================== | |
| * | |
| * Bootstrap Tourist v0.3.3-in progress | |
| * Copyright FFS 2019 | |
| * @ IGreatlyDislikeJavascript on Github | |
| * | |
| * This code is a fork of bootstrap-tour, with a lot of extra features | |
| * and fixes. You can read about why this fork exists here: | |
| * | |
| * https://github.com/sorich87/bootstrap-tour/issues/713 | |
| * | |
| * The entire purpose of this fork is to start rewriting bootstrap-tour | |
| * into native ES6 instead of the original coffeescript, and to implement | |
| * the features and fixes requested. | |
| * | |
| * I'm not a JS coder, so suggest you test very carefully and read the | |
| * docs before using. | |
| * | |
| * ======================================================================== | |
| * ENTIRELY BASED UPON: | |
| * | |
| * bootstrap-tour | |
| * http://bootstraptour.com | |
| * Copyright 2012-2015 Ulrich Sossou | |
| * | |
| * ======================================================================== | |
| * Licensed under the MIT License (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * https://opensource.org/licenses/MIT | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * ======================================================================== | |
| */ | |
| (function (window, factory) { | |
| if (typeof define === 'function' && define.amd) { | |
| return define(['jquery'], function (jQuery) { | |
| return window.Tour = factory(jQuery); | |
| }); | |
| } else if (typeof exports === 'object') { | |
| return module.exports = factory(require('jquery')); | |
| } else { | |
| return window.Tour = factory(window.jQuery); | |
| } | |
| })(window, function ($) { | |
| const DOMID_BACKDROP = "#tourBackdrop"; | |
| const DOMID_BACKDROP_TEMP = "#tourBackdrop-temp"; // used for @ibastevan zindex fix: https://github.com/IGreatlyDislikeJavascript/bootstrap-tourist/issues/38 | |
| const DOMID_HIGHLIGHT = "#tourHighlight"; | |
| const DOMID_HIGHLIGHT_TEMP = "#tourHighlight-temp"; // used for @ibastevan zindex fix: https://github.com/IGreatlyDislikeJavascript/bootstrap-tourist/issues/38 | |
| const DOMID_PREVENT = "#tourPrevent"; | |
| var Tour, document, objTemplates, objTemplatesButtonTexts; | |
| document = window.document; | |
| Tour = (function () { | |
| function Tour(options) | |
| { | |
| var storage; | |
| try | |
| { | |
| storage = window.localStorage; | |
| } | |
| catch (error) | |
| { | |
| storage = false; | |
| } | |
| // CUSTOMIZABLE TEXTS FOR BUTTONS | |
| // set defaults. We could of course add this to the $.extend({..localization: {} ...}) directly below. | |
| // However this is configured here, prior to the $.extend of options below, to enable a potential | |
| // future option of loading localization externally perhaps using $.getScript() etc. | |
| // | |
| // Note that these only affect the "default" templates (see objTemplates in this func below). The assumption is | |
| // that if user creates a tour with a custom template, they will name the buttons as required. We could force the | |
| // naming even in custom templates by identifying buttons in templates with data-role="...", but it seems more logical | |
| // NOT to do that... | |
| // | |
| // Finally, it's simple to allow different localization/button texts per tour step. To do this, alter the $.extend in | |
| // Tour.prototype.getStep() and subsequent code to load the per-step localization, identify the buttons by data-role, and | |
| // make the appropriate changes. That seems like a very niche requirement so it's not implemented here. | |
| objTemplatesButtonTexts = { | |
| prevButton: "Prev", | |
| nextButton: "Next", | |
| pauseButton: "Pause", | |
| resumeButton: "Resume", | |
| endTourButton: "End Tour" | |
| }; | |
| // GLOBAL OPTIONS take default options and overwrite with this tour options | |
| this._options = $.extend(true, | |
| { | |
| name: 'tour', | |
| steps: [], | |
| container: 'body', | |
| autoscroll: true, | |
| keyboard: true, | |
| storage: storage, | |
| debug: false, | |
| backdrop: false, | |
| backdropContainer: 'body', | |
| backdropOptions: { | |
| highlightOpacity: 0.9, | |
| highlightColor: "#FFF", | |
| backdropSibling: false, | |
| animation: { | |
| // can be string of css class or function signature: function(domElement, step) {} | |
| backdropShow: function(domElement, step) | |
| { | |
| domElement.fadeIn(); | |
| }, | |
| backdropHide: function(domElement, step) | |
| { | |
| domElement.fadeOut("slow") | |
| }, | |
| highlightShow: function(domElement, step) | |
| { | |
| // calling step.fnPositionHighlight() is the same as: | |
| // domElement.width($(step.element).outerWidth()).height($(step.element).outerHeight()).offset($(step.element).offset()); | |
| step.fnPositionHighlight(); | |
| domElement.fadeIn(); | |
| }, | |
| highlightTransition: "tour-highlight-animation", | |
| highlightHide: function(domElement, step) | |
| { | |
| domElement.fadeOut("slow") | |
| } | |
| }, | |
| }, | |
| redirect: true, | |
| orphan: false, | |
| showIfUnintendedOrphan: false, | |
| duration: false, | |
| delay: false, | |
| basePath: '', | |
| template: null, | |
| localization: { | |
| buttonTexts: objTemplatesButtonTexts | |
| }, | |
| framework: 'bootstrap3', | |
| sanitizeWhitelist: [], | |
| sanitizeFunction: null,// function(content) return sanitizedContent | |
| showProgressBar: true, | |
| showProgressText: true, | |
| getProgressBarHTML: null,//function(percent) {}, | |
| getProgressTextHTML: null,//function(stepNumber, percent, stepCount) {}, | |
| afterSetState: function (key, value) {}, | |
| afterGetState: function (key, value) {}, | |
| afterRemoveState: function (key) {}, | |
| onStart: function (tour) {}, | |
| onEnd: function (tour) {}, | |
| onShow: function (tour) {}, | |
| onShown: function (tour) {}, | |
| onHide: function (tour) {}, | |
| onHidden: function (tour) {}, | |
| onNext: function (tour) {}, | |
| onPrev: function (tour) {}, | |
| onPause: function (tour, duration) {}, | |
| onResume: function (tour, duration) {}, | |
| onRedirectError: function (tour) {}, | |
| onElementUnavailable: null, // function (tour, stepNumber) {}, | |
| onPreviouslyEnded: null, // function (tour) {}, | |
| onModalHidden: null, // function(tour, stepNumber) {} | |
| }, options); | |
| if($(this._options.backdropContainer).length == 0) | |
| { | |
| this._options.backdropContainer = "body"; | |
| } | |
| if(this._options.framework !== "bootstrap3" && this._options.framework !== "bootstrap4") | |
| { | |
| this._debug('Invalid framework specified: ' + this._options.framework); | |
| throw "Bootstrap Tourist: Invalid framework specified"; | |
| } | |
| // create the templates | |
| // SEARCH PLACEHOLDER: TEMPLATES LOCATION | |
| objTemplates = { | |
| bootstrap3 : '<div class="popover" role="tooltip"> <div class="arrow"></div> <h3 class="popover-title"></h3> <div class="popover-content"></div> <div class="popover-navigation"> <div class="btn-group"> <button class="btn btn-sm btn-default" data-role="prev">« ' + this._options.localization.buttonTexts.prevButton + '</button> <button class="btn btn-sm btn-default" data-role="next">' + this._options.localization.buttonTexts.nextButton + ' »</button> <button class="btn btn-sm btn-default" data-role="pause-resume" data-pause-text="' + this._options.localization.buttonTexts.pauseButton + '" data-resume-text="' + this._options.localization.buttonTexts.resumeButton + '">' + this._options.localization.buttonTexts.pauseButton + '</button> </div> <button class="btn btn-sm btn-default" data-role="end">' + this._options.localization.buttonTexts.endTourButton + '</button> </div> </div>', | |
| bootstrap4 : '<div class="popover" role="tooltip"> <div class="arrow"></div> <h3 class="popover-header"></h3> <div class="popover-body"></div> <div class="popover-navigation"> <div class="btn-group"> <button class="btn btn-sm btn-outline-secondary" data-role="prev">« ' + this._options.localization.buttonTexts.prevButton + '</button> <button class="btn btn-sm btn-outline-secondary" data-role="next">' + this._options.localization.buttonTexts.nextButton + ' »</button> <button class="btn btn-sm btn-outline-secondary" data-role="pause-resume" data-pause-text="' + this._options.localization.buttonTexts.pauseButton + '" data-resume-text="' + this._options.localization.buttonTexts.resumeButton + '">' + this._options.localization.buttonTexts.pauseButton + '</button> </div> <button class="btn btn-sm btn-outline-secondary" data-role="end">' + this._options.localization.buttonTexts.endTourButton + '</button> </div> </div>', | |
| }; | |
| // template option is default null. If not null after extend, caller has set a custom template, so don't touch it | |
| if(this._options.template === null) | |
| { | |
| // no custom template, so choose the template based on the framework | |
| if(objTemplates[this._options.framework] != null && objTemplates[this._options.framework] != undefined) | |
| { | |
| // there's a default template for the framework type specified in the options | |
| this._options.template = objTemplates[this._options.framework]; | |
| this._debug('Using framework template: ' + this._options.framework); | |
| } | |
| else | |
| { | |
| this._debug('Warning: ' + this._options.framework + ' specified for template (no template option set), but framework is unknown. Tour will not work!'); | |
| } | |
| } | |
| else | |
| { | |
| this._debug('Using custom template'); | |
| } | |
| if(typeof(this._options.sanitizeFunction) == "function") | |
| { | |
| this._debug("Using custom sanitize function in place of bootstrap - security implications, be careful"); | |
| } | |
| else | |
| { | |
| this._options.sanitizeFunction = null; | |
| this._debug("Extending Bootstrap sanitize options"); | |
| // no custom function, add our own | |
| // bootstrap 3.4.1 has whitelist functionality that strips tags from title, content etc of popovers and tooltips. Need to | |
| // add buttons to the whitelist otherwise the navigation buttons will be stripped from the popover content. | |
| // See issue: https://github.com/sorich87/bootstrap-tour/issues/723#issuecomment-471107788 | |
| // | |
| // ** UPDATE: BS3 and BS4 have the whitelist function. However: | |
| // BS3 uses $.fn.popover.Constructor.DEFAULTS.whiteList | |
| // BS4 uses $.fn.popover.Constructor.Default.whiteList | |
| // Even better, the CDN version of BS4 doesn't seem to include a whitelist property at all, which utterly screwed the first attempt at implementing | |
| // this, making it seem like my fix was working when in fact it was utterly broken. | |
| var defaultWhiteList = []; | |
| if(this._options.framework == "bootstrap4" && $.fn.popover.Constructor.Default.whiteList !== undefined) | |
| { | |
| defaultWhiteList = $.fn.popover.Constructor.Default.whiteList; | |
| } | |
| if(this._options.framework == "bootstrap3" && $.fn.popover.Constructor.DEFAULTS.whiteList !== undefined) | |
| { | |
| defaultWhiteList = $.fn.popover.Constructor.DEFAULTS.whiteList; | |
| } | |
| var whiteListAdditions = { | |
| "button": ["data-role", "style"], | |
| "img": ["style"], | |
| "div": ["style"] | |
| }; | |
| // whitelist is object with properties that are arrays. Need to merge "manually", as using $.extend with recursion will still overwrite the arrays . Try | |
| // var whiteList = $.extend(true, {}, defaultWhiteList, whiteListAdditions, this._options.sanitizeWhitelist); | |
| // and inspect the img property to see the issue - the default whitelist "src" (array elem 0) is overwritten with additions "style" | |
| // clone the default whitelist object first, otherwise we change the defaults for all of bootstrap! | |
| var whiteList = $.extend(true, {}, defaultWhiteList); | |
| // iterate the additions, and merge them into the defaults. We could just hammer them in manually but this is a little more expandable for the future | |
| $.each(whiteListAdditions, function( index, value ) | |
| { | |
| if(whiteList[index] == undefined) | |
| { | |
| whiteList[index] = []; | |
| } | |
| $.merge(whiteList[index], value); | |
| }); | |
| // and now do the same with the user specified whitelist in tour options | |
| $.each(this._options.sanitizeWhitelist, function( index, value ) | |
| { | |
| if(whiteList[index] == undefined) | |
| { | |
| whiteList[index] = []; | |
| } | |
| $.merge(whiteList[index], value); | |
| }); | |
| // save the merged whitelist back to the options, this is used by popover initialization when each step is shown | |
| this._options.sanitizeWhitelist = whiteList; | |
| } | |
| this._current = null; | |
| this.backdrops = []; | |
| return this; | |
| } | |
| Tour.prototype.addSteps = function (steps) { | |
| var j, | |
| len, | |
| step; | |
| for (j = 0, len = steps.length; j < len; j++) { | |
| step = steps[j]; | |
| this.addStep(step); | |
| } | |
| return this; | |
| }; | |
| Tour.prototype.addStep = function (step) { | |
| this._options.steps.push(step); | |
| return this; | |
| }; | |
| Tour.prototype.getStepCount = function() { | |
| return this._options.steps.length; | |
| }; | |
| Tour.prototype.getStep = function (i) { | |
| if (this._options.steps[i] != null) { | |
| if(typeof(this._options.steps[i].element) == "function") | |
| { | |
| this._options.steps[i].element = this._options.steps[i].element(); | |
| } | |
| // PER STEP OPTIONS: take the global options then override with this step's options. | |
| this._options.steps[i] = $.extend(true, | |
| { | |
| id: "step-" + i, | |
| path: '', | |
| host: '', | |
| placement: 'right', | |
| positioning:{ | |
| adjustRelative: null // this does nothing at the moment | |
| }, | |
| title: '', | |
| content: '<p></p>', | |
| next: i === this._options.steps.length - 1 ? -1 : i + 1, | |
| prev: i - 1, | |
| animation: true, | |
| container: this._options.container, | |
| autoscroll: this._options.autoscroll, | |
| backdrop: this._options.backdrop, | |
| //backdropOptions: this._options.backdropOptions, << SEE BELOW | |
| redirect: this._options.redirect, | |
| preventInteraction: false, | |
| orphan: this._options.orphan, | |
| showIfUnintendedOrphan: this._options.showIfUnintendedOrphan, | |
| duration: this._options.duration, | |
| delay: this._options.delay, | |
| delayOnElement: null, | |
| template: this._options.template, | |
| showProgressBar: this._options.showProgressBar, | |
| showProgressText: this._options.showProgressText, | |
| getProgressBarHTML: this._options.getProgressBarHTML, | |
| getProgressTextHTML: this._options.getProgressTextHTML, | |
| onShow: this._options.onShow, | |
| onShown: this._options.onShown, | |
| onHide: this._options.onHide, | |
| onHidden: this._options.onHidden, | |
| onNext: this._options.onNext, | |
| onPrev: this._options.onPrev, | |
| onPause: this._options.onPause, | |
| onResume: this._options.onResume, | |
| onRedirectError: this._options.onRedirectError, | |
| onElementUnavailable: this._options.onElementUnavailable, | |
| onModalHidden: this._options.onModalHidden, | |
| internalFlags: { | |
| elementModal: null, // will store the jq modal object for a step | |
| elementModalOriginal: null, // will store the original step.element string in steps that use a modal | |
| elementBootstrapSelectpicker: null // will store jq bootstrap select picker object | |
| } | |
| }, | |
| this._options.steps[i] | |
| ); | |
| // required so we don't overwrite the global options. | |
| this._options.steps[i].backdropOptions = $.extend(true, {}, this._options.backdropOptions, this._options.steps[i].backdropOptions); | |
| // safety to ensure consistent logic - reflex must == true if reflexOnly == true | |
| if(this._options.steps[i].reflexOnly == true) | |
| { | |
| this._options.steps[i].reflex = true; | |
| } | |
| return this._options.steps[i]; | |
| } | |
| }; | |
| // step flags are used to remember specific internal step data across a tour | |
| Tour.prototype._setStepFlag = function(stepNumber, flagName, value) | |
| { | |
| if(this._options.steps[stepNumber] != null) | |
| { | |
| this._options.steps[stepNumber].internalFlags[flagName] = value; | |
| } | |
| }; | |
| Tour.prototype._getStepFlag = function(stepNumber, flagName) | |
| { | |
| if(this._options.steps[stepNumber] != null) | |
| { | |
| return this._options.steps[stepNumber].internalFlags[flagName]; | |
| } | |
| }; | |
| //======================================================================================================================================= | |
| // Initiate tour and movement between steps | |
| Tour.prototype.init = function () | |
| { | |
| console.log('You should remove Tour.init() from your code. It\'s not required with Bootstrap Tourist'); | |
| } | |
| Tour.prototype.start = function () | |
| { | |
| // Test if this tour has previously ended, and start() was called | |
| if(this.ended()) | |
| { | |
| if(this._options.onPreviouslyEnded != null && typeof(this._options.onPreviouslyEnded) == "function") | |
| { | |
| this._debug('Tour previously ended, exiting. Call tour.restart() to force restart. Firing onPreviouslyEnded()'); | |
| this._options.onPreviouslyEnded(this); | |
| } | |
| else | |
| { | |
| this._debug('Tour previously ended, exiting. Call tour.restart() to force restart'); | |
| } | |
| return this; | |
| } | |
| // Call setCurrentStep() without params to start the tour using whatever step is recorded in localstorage. If no step recorded, tour starts | |
| // from first step. This provides the "resume tour" functionality. | |
| // Tour restart() simply removes the step from local storage | |
| this.setCurrentStep(); | |
| // Create the backdrop and highlight divs | |
| this._createOverlayElements(); | |
| this._initMouseNavigation(); | |
| this._initKeyboardNavigation(); | |
| // BS3: resize event must destroy and recreate both popper and background to ensure correct positioning | |
| // BS4: resize must destroy and recreate background, but popper.js handles popper positioning. | |
| var _this = this; | |
| $(window).on("resize.tour-" + _this._options.name, function() | |
| { | |
| _this.reshowCurrentStep(); | |
| } | |
| ); | |
| // Note: this call 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. Note that simply uncommenting this will not reinstate the code - _showPopoverAndOverlay automatically detects | |
| // if the current step is visible and will not reshow it. Therefore, to fully reinstate the "redraw on scroll" code, uncomment this and | |
| // also add appropriate code (to move popover & overlay) to the end of showPopover() | |
| // this._onScroll((function (_this) | |
| // { | |
| // return function () | |
| // { | |
| // return _this._showPopoverAndOverlay(_this._current); | |
| // }; | |
| // } | |
| // )); | |
| // start the tour - see if user provided onStart function, and if it returns a promise, obey that promise before calling showStep | |
| var promise = this._makePromise(this._options.onStart != null ? this._options.onStart(this) : void 0); | |
| this._callOnPromiseDone(promise, this.showStep, this._current); | |
| return this; | |
| }; | |
| Tour.prototype.next = function () { | |
| var promise; | |
| promise = this.hideStep(); | |
| return this._callOnPromiseDone(promise, this._showNextStep); | |
| }; | |
| Tour.prototype.prev = function () { | |
| var promise; | |
| promise = this.hideStep(); | |
| return this._callOnPromiseDone(promise, this._showPrevStep); | |
| }; | |
| Tour.prototype.goTo = function (i) { | |
| var promise; | |
| this._debug("goTo step " + i); | |
| promise = this.hideStep(); | |
| return this._callOnPromiseDone(promise, this.showStep, i); | |
| }; | |
| Tour.prototype.end = function () | |
| { | |
| this._debug("Tour.end() called"); | |
| var endHelper, | |
| promise; | |
| endHelper = (function (_this) { | |
| return function (e) { | |
| $(document).off("click.tour-" + _this._options.name); | |
| $(document).off("keyup.tour-" + _this._options.name); | |
| $(window).off("resize.tour-" + _this._options.name); | |
| $(window).off("scroll.tour-" + _this._options.name); | |
| _this._setState('end', 'yes'); | |
| _this._clearTimer(); | |
| $(".tour-step-element-reflex").removeClass("tour-step-element-reflex"); | |
| $(".tour-step-element-reflexOnly").removeClass("tour-step-element-reflexOnly"); | |
| _this._hideBackdrop(); | |
| _this._destroyOverlayElements(); | |
| if (_this._options.onEnd != null) | |
| { | |
| return _this._options.onEnd(_this); | |
| } | |
| }; | |
| })(this); | |
| promise = this.hideStep(); | |
| return this._callOnPromiseDone(promise, endHelper); | |
| }; | |
| Tour.prototype.ended = function () { | |
| return this._getState('end') == 'yes'; | |
| }; | |
| Tour.prototype.restart = function () | |
| { | |
| this._removeState('current_step'); | |
| this._removeState('end'); | |
| this._removeState('redirect_to'); | |
| return this.start(); | |
| }; | |
| Tour.prototype.pause = function () { | |
| var step; | |
| step = this.getStep(this._current); | |
| if (!(step && step.duration)) { | |
| return this; | |
| } | |
| this._paused = true; | |
| this._duration -= new Date().getTime() - this._start; | |
| window.clearTimeout(this._timer); | |
| this._debug("Paused/Stopped step " + (this._current + 1) + " timer (" + this._duration + " remaining)."); | |
| if (step.onPause != null) { | |
| return step.onPause(this, this._duration); | |
| } | |
| }; | |
| Tour.prototype.resume = function () { | |
| var step; | |
| step = this.getStep(this._current); | |
| if (!(step && step.duration)) { | |
| return this; | |
| } | |
| this._paused = false; | |
| this._start = new Date().getTime(); | |
| this._duration = this._duration || step.duration; | |
| this._timer = window.setTimeout(( function (_this) | |
| { | |
| return function () | |
| { | |
| if (_this._isLast()) | |
| { | |
| return _this.end(); | |
| } | |
| else | |
| { | |
| return _this.next(); | |
| } | |
| }; | |
| })(this), this._duration); | |
| this._debug("Started step " + (this._current + 1) + " timer with duration " + this._duration); | |
| if ((step.onResume != null) && this._duration !== step.duration) { | |
| return step.onResume(this, this._duration); | |
| } | |
| }; | |
| // fully closes and reopens the current step, triggering all callbacks etc | |
| Tour.prototype.reshowCurrentStep = function() | |
| { | |
| this._debug("Reshowing current step " + this.getCurrentStepIndex()); | |
| var promise; | |
| promise = this.hideStep(); | |
| return this._callOnPromiseDone(promise, this.showStep, this._current); | |
| }; | |
| //======================================================================================================================================= | |
| // hides current step | |
| Tour.prototype.hideStep = function () | |
| { | |
| var hideDelay, | |
| hideStepHelper, | |
| promise, | |
| step; | |
| step = this.getStep(this.getCurrentStepIndex()); | |
| if (!step) | |
| { | |
| return; | |
| } | |
| this._clearTimer(); | |
| promise = this._makePromise(step.onHide != null ? step.onHide(this, this.getCurrentStepIndex()) : void 0); | |
| hideStepHelper = (function (_this) | |
| { | |
| return function (e) | |
| { | |
| var $element; | |
| $element = $(step.element); | |
| if (!($element.data('bs.popover') || $element.data('popover'))) | |
| { | |
| $element = $('body'); | |
| } | |
| if(_this._options.framework == "bootstrap3") | |
| { | |
| $element.popover('destroy'); | |
| } | |
| if(_this._options.framework == "bootstrap4") | |
| { | |
| $element.popover('dispose'); | |
| } | |
| $element.removeClass("tour-" + _this._options.name + "-element tour-" + _this._options.name + "-" + _this.getCurrentStepIndex() + "-element").removeData('bs.popover'); | |
| if (step.reflex) | |
| { | |
| $element.removeClass('tour-step-element-reflex').off((_this._reflexEvent(step.reflex)) + ".tour-" + _this._options.name); | |
| $element.removeClass('tour-step-element-reflexOnly'); | |
| } | |
| // now handled by updateOverlayElements | |
| //_this._hideOverlayElements(step); | |
| _this._unfixBootstrapSelectPickerZindex(step); | |
| // If this step was pointed at a modal, revert changes to the step.element. See the notes in showStep for explanation | |
| var tmpModalOriginalElement = _this._getStepFlag(_this.getCurrentStepIndex(), "elementModalOriginal"); | |
| if(tmpModalOriginalElement != null) | |
| { | |
| _this._setStepFlag(_this.getCurrentStepIndex(), "elementModalOriginal", null); | |
| step.element = tmpModalOriginalElement; | |
| } | |
| if (step.onHidden != null) | |
| { | |
| return step.onHidden(_this); | |
| } | |
| }; | |
| })(this); | |
| hideDelay = step.delay.hide || step.delay; | |
| if ({} | |
| .toString.call(hideDelay) === '[object Number]' && hideDelay > 0) { | |
| this._debug("Wait " + hideDelay + " milliseconds to hide the step " + (this._current + 1)); | |
| window.setTimeout((function (_this) { | |
| return function () { | |
| return _this._callOnPromiseDone(promise, hideStepHelper); | |
| }; | |
| })(this), hideDelay); | |
| } else { | |
| this._callOnPromiseDone(promise, hideStepHelper); | |
| } | |
| return promise; | |
| }; | |
| // loads all required step info and prepares to show | |
| Tour.prototype.showStep = function (i) { | |
| var path, | |
| promise, | |
| showDelay, | |
| showStepHelper, | |
| skipToPrevious, | |
| step, | |
| $element; | |
| if(this.ended()) | |
| { | |
| // Note: see feature addition #12 and "onPreviouslyEnded" option to understand when this._options.onEnd is called vs this._options.onPreviouslyEnded() | |
| this._debug('Tour ended, showStep prevented.'); | |
| if(this._options.onEnd != null) | |
| { | |
| this._options.onEnd(this); | |
| } | |
| return this; | |
| } | |
| step = this.getStep(i); | |
| if (!step) { | |
| return; | |
| } | |
| skipToPrevious = i < this._current; | |
| promise = this._makePromise(step.onShow != null ? step.onShow(this, i) : void 0); | |
| this.setCurrentStep(i); | |
| path = (function () { | |
| switch ({} | |
| .toString.call(step.path)) { | |
| case '[object Function]': | |
| return step.path(); | |
| case '[object String]': | |
| return this._options.basePath + step.path; | |
| default: | |
| return step.path; | |
| } | |
| }).call(this); | |
| if (step.redirect && this._isRedirect(step.host, path, document.location)) { | |
| this._redirect(step, i, path); | |
| if (!this._isJustPathHashDifferent(step.host, path, document.location)) { | |
| return; | |
| } | |
| } | |
| // will be set to element <div class="modal"> if modal in use | |
| var $modalObject = null; | |
| // is element a modal? | |
| if(step.orphan === false && ($(step.element).hasClass("modal") || $(step.element).data('bs.modal'))) | |
| { | |
| // element is exactly the modal div | |
| $modalObject = $(step.element); | |
| // This is a hack solution. Original Tour uses step.element in multiple places and converts to jquery object as needed. This func uses $element, | |
| // but multiple other funcs simply use $(step.element) instead - keeping the original string element id in the step data and using jquery as needed. | |
| // This creates problems with dialogs, especially BootStrap Dialog plugin - in code terms, the dialog is everything from <div class="modal-dialog">, | |
| // but the actual visible positioned part of the dialog is <div class="modal-dialog"><div class="modal-content">. 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? Find the parent modal | |
| if($modalObject === null && $element.parents(".modal:first").length) | |
| { | |
| // find the parent modal div | |
| $modalObject = $element.parents(".modal:first"); | |
| } | |
| // Is this step a modal? | |
| if($modalObject && $modalObject.length > 0) | |
| { | |
| // Yes, set up the modal helper - called when the modal is hidden. This enables the onModalHidden tour option. | |
| 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 = 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); | |
| // thanks to @eformx for finding this bug! | |
| if (_this._isLast()) | |
| { | |
| _this._debug("Modal close reached end of tour"); | |
| return _this.end(); | |
| } | |
| else | |
| { | |
| _this._debug("Modal close: next step called"); | |
| return _this.next(); | |
| } | |
| }; | |
| }(this, $modalObject); | |
| $modalObject.off("hidden.bs.modal", funcModalHelper).on("hidden.bs.modal", funcModalHelper); | |
| } | |
| // Helper function to actually show the popover using _showPopoverAndOverlay. | |
| // Note the flow - this is called immediately unless delayOnElement is set. If delayOnElement is set, this | |
| // func will be called if (a) the element appears, or (b) the element doesn't appear in the timeout. | |
| // Therefore this helper func MUST handle unintended orphans | |
| showStepHelper = ( function (_this) | |
| { | |
| return function (e) | |
| { | |
| if (_this._isOrphan(step)) | |
| { | |
| // Is this an unintended orphan? | |
| if (step.orphan === false && step.showIfUnintendedOrphan === 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; | |
| } | |
| if (step.orphan === false && step.showIfUnintendedOrphan === true) | |
| { | |
| // it's an unintended orphan, and global or step options still want to show it | |
| _this._debug("Show the unintended orphan step " + (_this._current + 1) + ". showIfUnintendedOrphan option is true."); | |
| } | |
| else | |
| { | |
| // It's an intended orphan | |
| _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) | |
| { | |
| var 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) | |
| { | |
| var 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 Tourist: '" + 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); | |
| // handles all show, hide and move of the background and highlight | |
| this._updateBackdropElements(step); | |
| // Show the preventInteraction overlay etc | |
| this._updateOverlayElements(step); | |
| // Required to fix the z index issue with BS select dropdowns | |
| 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'; | |
| // If step is an intended or unintended orphan, and reflexOnly is set, show a warning. | |
| if(step.reflexOnly) | |
| { | |
| this._debug("Step is an orphan, and reflexOnly is set: ignoring reflexOnly"); | |
| } | |
| } | |
| $element = $(step.element); | |
| $element.addClass("tour-" + this._options.name + "-element tour-" + this._options.name + "-" + i + "-element"); | |
| if (step.reflex && !isOrphan) | |
| { | |
| $element.addClass('tour-step-element-reflex'); | |
| $element.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.end(); | |
| } | |
| else | |
| { | |
| return _this.next(); | |
| } | |
| }; | |
| })(this)); | |
| if(step.reflexOnly) | |
| { | |
| // this pseudo-class is used to quickly identify reflexOnly steps in handlers / code that don't have access to tour.step (without | |
| // costly reloading) but need to know about reflexOnly. For example, obeying reflexOnly in keyboard handler. Solves | |
| // https://github.com/IGreatlyDislikeJavascript/bootstrap-tourist/issues/45 | |
| $element.addClass('tour-step-element-reflexOnly'); | |
| // Only disable the next button if this step is NOT an orphan. | |
| // This is difficult to achieve robustly because tour creator can use a custom template. Instead of trying to manually | |
| // edit the template - which must be a string to be passed to popover creation - use jquery to find the element, hide | |
| // it, then use the resulting DOM code/string to search and replace | |
| // Find "next" object (button, href, etc), create a copy | |
| var $objNext = $(step.template).find('[data-role="next"]').clone(); | |
| if($objNext.length) | |
| { | |
| // Get the DOM code for the object | |
| var strNext = $objNext[0].outerHTML; | |
| $objNext.hide(); | |
| // Get the DOM code for the hidden object | |
| var strHidden = $objNext[0].outerHTML; | |
| // string replace it in the template | |
| step.template = step.template.replace(strNext, strHidden); | |
| } | |
| } | |
| } | |
| 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 = '<div class="progress"><div class="progress-bar progress-bar-striped" role="progressbar" style="width: ' + percentProgress + '%;"></div></div>' + content; | |
| } | |
| } | |
| if(step.showProgressText) | |
| { | |
| if(typeof(step.getProgressTextHTML) == "function") | |
| { | |
| title += step.getProgressTextHTML(i, percentProgress, this.getStepCount()); | |
| } | |
| else | |
| { | |
| if(this._options.framework == "bootstrap3") | |
| { | |
| title += '<span class="pull-right">' + (i + 1) + '/' + this.getStepCount() + '</span>'; | |
| } | |
| if(this._options.framework == "bootstrap4") | |
| { | |
| title += '<span class="float-right">' + (i + 1) + '/' + this.getStepCount() + '</span>'; | |
| } | |
| } | |
| } | |
| // 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; | |
| // Allow manual repositioning of the popover | |
| // THIS DOESN'T WORK - popper.js will only adjust on one axis even if both axis are specified... | |
| if(step.positioning.adjustRelative !== null && step.positioning.adjustRelative.length > 0) | |
| { | |
| if(typeof step.positioning.adjustRelative == "function") | |
| { | |
| popOpts.offset = step.positioning.adjustRelative(); | |
| } | |
| else | |
| { | |
| popOpts.offset = step.positioning.adjustRelative; | |
| } | |
| } | |
| } | |
| } | |
| $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); | |
| } | |
| // Cannot do this here due to new option showIfUnintendedOrphan - an unintended orphan with reflex/reflexonly will create a | |
| // tour step that can't be moved on from! This must be done in showPopover, which is called after the step is loaded and any | |
| // delayOnElement timeouts etc have occurred, meaning we know for certain in _showPopover whether the step is an orphan | |
| // if (step.reflexOnly) { | |
| // $next.hide(); | |
| // } | |
| if (!step.duration) { | |
| $resume.remove(); | |
| } | |
| return $template.clone().wrap('<div>').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: | |
| // arrow right | |
| if($(".tour-step-element-reflexOnly").length == 0) | |
| { | |
| e.preventDefault(); | |
| if(_this._isLast()) | |
| { | |
| return _this.end(); | |
| } | |
| else | |
| { | |
| return _this.next(); | |
| } | |
| } | |
| break; | |
| case 37: | |
| // arrow left | |
| if($(".tour-step-element-reflexOnly").length == 0) | |
| { | |
| e.preventDefault(); | |
| if (_this._current > 0) | |
| { | |
| return _this.prev(); | |
| } | |
| } | |
| break; | |
| case 27: | |
| // escape | |
| e.preventDefault(); | |
| return _this.end(); | |
| break; | |
| } | |
| }; | |
| })(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 and at zindex of highlight element | |
| $selectpicker.parent().css("z-index", "1111"); | |
| // 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"); | |
| } | |
| }; | |
| // =================================================================================================================================================== | |
| // NEW OVERLAY CODE | |
| // | |
| // NOTE: "backdrop" refers to all the elements required to create the "dark background with a highlight" function, i.e.: a background div and | |
| // a highlight div. | |
| // =================================================================================================================================================== | |
| // Actually creates the 3 divs for functionality | |
| Tour.prototype._createOverlayElements = function () | |
| { | |
| // the .substr(1) is because the DOMID_ consts start with # for jq object ease... | |
| var $backdrop = $('<div class="tour-backdrop" id="' + DOMID_BACKDROP.substr(1) + '"></div>'); | |
| var $highlight = $('<div class="tour-highlight" id="' + DOMID_HIGHLIGHT.substr(1) + '" style="width:0px;height:0px;top:0px;left:0px;"></div>'); | |
| // _updateOverlayElements creates and destroys prevent div as required | |
| //var $preventDiv = $('<div class="tour-prevent" id="' + DOMID_PREVENT.substr(1) + '" style="width:0px;height:0px;top:0px;left:0px;"></div>'); | |
| //var $debug = $('<!-- debug -->'); | |
| //$("body").append($debug); | |
| if ($(DOMID_BACKDROP).length === 0) | |
| { | |
| $(this._options.backdropContainer).append($backdrop); | |
| } | |
| if ($(DOMID_HIGHLIGHT).length === 0) | |
| { | |
| $(this._options.backdropContainer).append($highlight); | |
| } | |
| // if ($(DOMID_PREVENT).length === 0) | |
| // { | |
| // $(this._options.backdropContainer).append($preventDiv); | |
| // } | |
| }; | |
| Tour.prototype._destroyOverlayElements = function(step) | |
| { | |
| $(DOMID_BACKDROP).remove(); | |
| $(DOMID_HIGHLIGHT).remove(); | |
| $(DOMID_PREVENT).remove(); | |
| $(".tour-highlight-element").removeClass("tour-highlight-element"); | |
| }; | |
| // Hides the background and highlight. Caller is responsible for ensuring step wants hidden | |
| // backdrop | |
| Tour.prototype._hideBackdrop = function(step) | |
| { | |
| var step = step || null; | |
| if(step) | |
| { | |
| // No backdrop? No need for highlight | |
| this._hideHighlightOverlay(step); | |
| // Does global or this step specify a function for the backdrop layer hide? | |
| if(typeof step.backdropOptions.animation.backdropHide == "function") | |
| { | |
| // pass DOM element jq object to function | |
| step.backdropOptions.animation.backdropHide($(DOMID_BACKDROP)); | |
| } | |
| else | |
| { | |
| // must be a CSS class | |
| $(DOMID_BACKDROP).addClass(step.backdropOptions.animation.backdropHide); | |
| $(DOMID_BACKDROP).hide(0, function() | |
| { | |
| $(this).removeClass(step.backdropOptions.animation.backdropHide); | |
| }); | |
| } | |
| } | |
| else | |
| { | |
| $(DOMID_BACKDROP).hide(0); | |
| $(DOMID_HIGHLIGHT).hide(0); | |
| $(DOMID_BACKDROP_TEMP).remove(); | |
| $(DOMID_HIGHLIGHT_TEMP).remove(); | |
| } | |
| }; | |
| // Shows the backdrop (backdrop + highlight elements if not orphan). Caller is responsible for ensuring step really wants a visible | |
| // backdrop | |
| Tour.prototype._showBackdrop = function (step) | |
| { | |
| var step = step || null; | |
| // Ensure we're always starting with a clean, hidden backdrop - this ensures any previous step.backdropOptions.animation.* functions | |
| // haven't messed with the classes | |
| $(DOMID_BACKDROP).removeClass().addClass("tour-backdrop").hide(0); | |
| if(step) | |
| { | |
| // Does global or this step specify a function for the backdrop layer show? | |
| if(typeof step.backdropOptions.animation.backdropShow == "function") | |
| { | |
| // pass DOM element jq object to function | |
| step.backdropOptions.animation.backdropShow($(DOMID_BACKDROP)); | |
| } | |
| else | |
| { | |
| // must be a CSS class | |
| $(DOMID_BACKDROP).addClass(step.backdropOptions.animation.backdropShow); | |
| $(DOMID_BACKDROP).show(0, function() | |
| { | |
| $(this).removeClass(step.backdropOptions.animation.backdropShow); | |
| }); | |
| } | |
| // Now handle the highlight layer. The backdrop and highlight layers operate together to create the visual backdrop, but are handled | |
| // as separate DOM and code elements. | |
| if(this._isOrphan(step)) | |
| { | |
| // Orphan step will never require a highlight, as there's no element | |
| if($(DOMID_HIGHLIGHT).is(':visible')) | |
| { | |
| this._hideHighlightOverlay(step); | |
| } | |
| else | |
| { | |
| // orphan step with highlight layer already hidden - do nothing | |
| } | |
| } | |
| else | |
| { | |
| // Not an orphan, so requires a highlight layer. | |
| if($(DOMID_HIGHLIGHT).is(':visible')) | |
| { | |
| // Already visible, so this is a transition - move from 1 position to another. This shouldn't be possible, | |
| // as a call to showBackdrop() logically means the backdrop is hidden, therefore the highlight is hidden. Kept for safety. | |
| this._positionHighlightOverlay(step); | |
| } | |
| else | |
| { | |
| // Not visible, this is a show | |
| this._showHighlightOverlay(step); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| $(DOMID_BACKDROP).show(0); | |
| $(DOMID_HIGHLIGHT).show(0); | |
| } | |
| }; | |
| // Creates an object representing the current step with a subset of properties and functions, for | |
| // tour creator to use when passing functions to step.backdropOptions.animation options | |
| Tour.prototype._createStepSubset = function (step) | |
| { | |
| var _this = this; | |
| var _stepElement = $(step.element); | |
| var stepSubset = { | |
| element: _stepElement, | |
| container: step.container, | |
| autoscroll: step.autoscroll, | |
| backdrop: step.backdrop, | |
| preventInteraction: step.preventInteraction, | |
| isOrphan: this._isOrphan(step), | |
| orphan: step.orphan, | |
| showIfUnintendedOrphan: step.showIfUnintendedOrphan, | |
| duration: step.duration, | |
| delay: step.delay, | |
| fnPositionHighlight: function() | |
| { | |
| _this._debug("Positioning highlight (fnPositionHighlight) over step element " + _stepElement[0].id + ":\nWidth = " + _stepElement.outerWidth() + ", height = " + _stepElement.outerHeight() + "\nTop: " + _stepElement.offset().top + ", left: " + _stepElement.offset().left); | |
| $(DOMID_HIGHLIGHT).width(_stepElement.outerWidth()).height(_stepElement.outerHeight()).offset(_stepElement.offset()); | |
| }, | |
| }; | |
| return stepSubset; | |
| }; | |
| // Shows the highlight and applies class to highlighted element | |
| Tour.prototype._showHighlightOverlay = function (step) | |
| { | |
| // safety check, ensure no other elem has the highlight class | |
| var $elemTmp = $(".tour-highlight-element"); | |
| if($elemTmp.length > 0) | |
| { | |
| $elemTmp.removeClass('tour-highlight-element'); | |
| } | |
| // Is this a modal - we must set the zindex on the modal element, not the modal-content element | |
| var $modalCheck = $(step.element).parents(".modal:first"); | |
| if($modalCheck.length) | |
| { | |
| $modalCheck.addClass('tour-highlight-element'); | |
| } | |
| else | |
| { | |
| $(step.element).addClass('tour-highlight-element'); | |
| } | |
| // Ensure we're always starting with a clean, hidden highlight - this ensures any previous step.backdropOptions.animation.* functions | |
| // haven't messed with the classes | |
| $(DOMID_HIGHLIGHT).removeClass().addClass("tour-highlight").hide(0); | |
| if(typeof step.backdropOptions.animation.highlightShow == "function") | |
| { | |
| // pass DOM element jq object to function. Function is completely responsible for positioning and showing. | |
| // dupe the step to avoid function messing with original object. | |
| step.backdropOptions.animation.highlightShow($(DOMID_HIGHLIGHT), this._createStepSubset(step)); | |
| } | |
| else | |
| { | |
| // must be a CSS class. Give a default animation | |
| $(DOMID_HIGHLIGHT).css( { | |
| "opacity": step.backdropOptions.highlightOpacity, | |
| "background-color": step.backdropOptions.highlightColor | |
| }); | |
| $(DOMID_HIGHLIGHT).width(0).height(0).offset({ top: 0, left: 0 }); | |
| $(DOMID_HIGHLIGHT).show(0); | |
| $(DOMID_HIGHLIGHT).addClass(step.backdropOptions.animation.highlightShow); | |
| $(DOMID_HIGHLIGHT).width($(step.element).outerWidth()).height($(step.element).outerHeight()).offset($(step.element).offset()); | |
| $(DOMID_HIGHLIGHT).one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() | |
| { | |
| $(DOMID_HIGHLIGHT).removeClass(step.backdropOptions.animation.highlightShow); | |
| }); | |
| } | |
| }; | |
| // Repositions a currently visible highlight | |
| Tour.prototype._positionHighlightOverlay = function (step) | |
| { | |
| // safety check, ensure no other elem has the highlight class | |
| var $elemTmp = $(".tour-highlight-element"); | |
| if($elemTmp.length > 0) | |
| { | |
| $elemTmp.removeClass('tour-highlight-element'); | |
| } | |
| // Is this a modal - we must set the zindex on the modal element, not the modal-content element | |
| var $modalCheck = $(step.element).parents(".modal:first"); | |
| if($modalCheck.length) | |
| { | |
| $modalCheck.addClass('tour-highlight-element'); | |
| } | |
| else | |
| { | |
| $(step.element).addClass('tour-highlight-element'); | |
| } | |
| if(typeof step.backdropOptions.animation.highlightTransition == "function") | |
| { | |
| // Don't clean existing classes - this allows tour coder to fully control the highlight between steps | |
| // pass DOM element jq object to function. Function is completely responsible for positioning and showing. | |
| // dupe the step to avoid function messing with original object. | |
| step.backdropOptions.animation.highlightTransition($(DOMID_HIGHLIGHT), this._createStepSubset(step)); | |
| } | |
| else | |
| { | |
| // must be a CSS class. Start by cleaning all other classes | |
| $(DOMID_HIGHLIGHT).removeClass().addClass("tour-highlight"); | |
| // obey step options | |
| $(DOMID_HIGHLIGHT).css( { | |
| "opacity": step.backdropOptions.highlightOpacity, | |
| "background-color": step.backdropOptions.highlightColor | |
| }); | |
| // add transition animations | |
| $(DOMID_HIGHLIGHT).addClass(step.backdropOptions.animation.highlightTransition); | |
| $(DOMID_HIGHLIGHT).width($(step.element).outerWidth()).height($(step.element).outerHeight()).offset($(step.element).offset()); | |
| $(DOMID_HIGHLIGHT).one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() | |
| { | |
| $(DOMID_HIGHLIGHT).removeClass(step.backdropOptions.animation.highlightTransition); | |
| }); | |
| } | |
| }; | |
| Tour.prototype._hideHighlightOverlay = function (step) | |
| { | |
| // remove the highlight class | |
| $(".tour-highlight-element").removeClass('tour-highlight-element'); | |
| if(typeof step.backdropOptions.animation.highlightHide == "function") | |
| { | |
| // pass DOM element jq object to function. Function is completely responsible for positioning and showing. | |
| // dupe the step to avoid function messing with original object. | |
| step.backdropOptions.animation.highlightHide($(DOMID_HIGHLIGHT), this._createStepSubset(step)); | |
| } | |
| else | |
| { | |
| // must be a CSS class | |
| $(DOMID_HIGHLIGHT).addClass(step.backdropOptions.animation.highlightHide); | |
| //$(DOMID_HIGHLIGHT).width(0).height(0).offset({ top: 0, left: 0 }); | |
| $(DOMID_HIGHLIGHT).one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() | |
| { | |
| // ensure we end with a clean div | |
| $(DOMID_HIGHLIGHT).removeClass().addClass("tour-highlight"); | |
| $(DOMID_HIGHLIGHT).hide(0); | |
| }); | |
| } | |
| }; | |
| // Moves, shows or hides the backdrop and highlight element to match the specified step | |
| Tour.prototype._updateBackdropElements = function (step) | |
| { | |
| // Change to backdrop visibility required? (step.backdrop != current $(DOMID_BACKDROP) visibility) | |
| if(step.backdrop != $(DOMID_BACKDROP).is(':visible')) | |
| { | |
| // step backdrop not in sync with actual backdrop. Deal with it! | |
| if(step.backdrop) | |
| { | |
| // handles both the background div and the highlight layer | |
| this._showBackdrop(step); | |
| } | |
| else | |
| { | |
| this._hideBackdrop(step); | |
| } | |
| } | |
| else | |
| { | |
| // backdrop is in the correct state (visible/non visible) for this step | |
| if(step.backdrop) | |
| { | |
| // Step includes backdrop, and backdrop is already visible. | |
| // Is this step an orphan (i.e.: no highlight)? | |
| if(this._isOrphan(step)) | |
| { | |
| // Orphan doesn't require highlight as no element. Is the highlight currently visible? (from the previous step) | |
| if($(DOMID_HIGHLIGHT).is(':visible')) | |
| { | |
| // Need to hide it | |
| this._hideHighlightOverlay(step); | |
| } | |
| else | |
| { | |
| // Highlight not visible, not required. Do nothing. | |
| } | |
| } | |
| else | |
| { | |
| // Highlight required | |
| if($(DOMID_HIGHLIGHT).is(':visible')) | |
| { | |
| // Transition it | |
| this._positionHighlightOverlay(step); | |
| } | |
| else | |
| { | |
| // Show it | |
| this._showHighlightOverlay(step); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // Step does not include backdrop, backdrop is already hidden. | |
| // Ensure highlight is also hidden - safety check as hideBackdrop also hides highlight | |
| if($(DOMID_HIGHLIGHT).is(':visible')) | |
| { | |
| this._hideHighlightOverlay(step); | |
| } | |
| } | |
| } | |
| // purpose of this code is due to elements with position: fixed and z-index: https://github.com/IGreatlyDislikeJavascript/bootstrap-tourist/issues/38 | |
| $(DOMID_BACKDROP_TEMP).remove(); | |
| $(DOMID_HIGHLIGHT_TEMP).remove(); | |
| if (step.backdropOptions.backdropSibling == true) | |
| { | |
| $(DOMID_HIGHLIGHT).addClass('tour-behind'); | |
| $(DOMID_BACKDROP).addClass('tour-zindexFix'); | |
| $(DOMID_HIGHLIGHT).clone().prop('id', DOMID_HIGHLIGHT_TEMP.substring(1)).removeClass('tour-behind').insertAfter(".tour-highlight-element"); | |
| $(DOMID_BACKDROP).clone().prop('id', DOMID_BACKDROP_TEMP.substring(1)).removeClass('tour-zindexFix').insertAfter(".tour-highlight-element"); | |
| } | |
| else | |
| { | |
| $(DOMID_HIGHLIGHT).removeClass('tour-behind'); | |
| $(DOMID_BACKDROP).removeClass('tour-zindexFix'); | |
| } | |
| }; | |
| // Updates visibility of the preventInteraction div and any other overlay elements added in future features | |
| Tour.prototype._updateOverlayElements = function (step) | |
| { | |
| // check if the popover for the current step already exists (is this a redraw) | |
| if (step.preventInteraction) | |
| { | |
| this._debug("preventInteraction == true, adding overlay"); | |
| if ($(DOMID_PREVENT).length === 0) | |
| { | |
| $('<div class="tour-prevent" id="' + DOMID_PREVENT.substr(1) + '" style="width:0px;height:0px;top:0px;left:0px;"></div>').insertAfter(DOMID_HIGHLIGHT); | |
| } | |
| $(DOMID_PREVENT).width($(step.element).outerWidth()).height($(step.element).outerHeight()).offset($(step.element).offset()); | |
| } | |
| else | |
| { | |
| $(DOMID_PREVENT).remove(); | |
| } | |
| }; | |
| // =================================================================================================================================================== | |
| // END NEW OVERLAY CODE | |
| // =================================================================================================================================================== | |
| 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; | |
| }); |