/*! * jquery.tour.js 0.0.2 - https://github.com/yckart/jquery.tour.js * A frontend presentation tool. * * Based upon https://github.com/twanlass/tour.js * * Copyright (c) 2013 Yannick Albert (http://yckart.com) * Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php). * 2013/02/09 */ (function($, window){ $.tour = function (TourSteps, TourCompleteCallback) { var Tour = { startFrom: 0, // override this to start the tour from a specified point offsetFudge: 15, // adjusts distance from target for tour dialog allowSkip: false, // coming in a future update funnelID: "tourV1", // we'll pass this to Mixpanel to segement on and guage future funnel improvment init: function () { $('body').prepend('
'); this.showStep(); }, // increment steps or pause if we're waiting on a trigger to advance nextStep: function () { var latent = TourSteps[this.startFrom].waitForTrigger; // should we proceed? this.startFrom += 1; this[this.startFrom < TourSteps.length && !latent ? 'showStep' : 'tourComplete'](); }, // build dialog content, set focus on elements, and finally fade in the step showStep: function () { var parent = this; this.setupSelectorsLive(); $('.tour_item').removeClass('active'); this.getContent(); this.setDialogPos(TourSteps[this.startFrom].position); $('*').bind('click', { stop: this.startFrom }, this.tourClickHandler); if (!$('#tour_mask').is(":visible")) { $('#tour_mask').fadeIn(); } $(TourSteps[this.startFrom].selector).addClass('active').fadeIn(); $('#tour_dialog').fadeIn(function () { parent.dialogVisible(); }); }, stepComplete: function () { this.mpEvent(TourSteps[this.startFrom].actionName); $('*').unbind('click', this.tourClickHandler); var parent = this; $('#tour_dialog').fadeOut(function () { parent.nextStep(); this.inProgress = true; }); }, tourComplete: function () { $('#tour_dialog').fadeOut(); // sipmle check to see if we're pausing or at the end of our tour step // if we're just pausing, we want to keep focus on the area until we move forward if (this.startFrom == TourSteps.length) { $('#tour_mask').fadeOut(function () { $('.tour_item').removeClass('active'); if (typeof TourCompleteCallback == 'function') { TourCompleteCallback.call(); } }); } }, // To use: include MixPanel js (see mixpanel.com). Set identity or person info *before* the tour starts. // Add a "actionName" string to each step and upon user completion it will be automatically sent to MixPanel for funnel tracking mpEvent: function (eventName) { if (eventName) { mixpanel.track(eventName, { "funnelVersion": this.funnelID }); } }, getContent: function () { var message = TourSteps[this.startFrom].msg; message += TourSteps[this.startFrom].btnMsg ? "
" + TourSteps[this.startFrom].btnMsg + "
" : ""; $('#tour_dialog .msg').html(message); }, setDialogPos: function (position) { // @todo - need to calculate arrow position in px instead of % var selArray = TourSteps[this.startFrom].selector.split(","); var target = $(selArray[0]); var dialog = $('#tour_dialog'); var top, left; // added check to see if the target exists if not go to next step if (target.length === 0) { this.nextStep(); } else { switch (position) { case "top": top = target.offset().top - dialog.outerHeight() - this.offsetFudge; left = target.offset().left + target.outerWidth() / 2 - dialog.outerWidth() / 2; break; case "right": top = target.offset().top; left = target.offset().left + target.outerWidth() + this.offsetFudge; break; case "bottom": top = target.offset().top + target.outerHeight() + this.offsetFudge; left = target.offset().left + target.outerWidth() / 2 - dialog.outerWidth() / 2; break; case "left": top = target.offset().top; left = target.offset().left - dialog.outerWidth() - this.offsetFudge; break; case "center": // screen center hor & vert top = Math.max(0, (($(window).height() - dialog.outerHeight()) / 2) + $(window).scrollTop()); left = Math.max(0, (($(window).width() - dialog.outerWidth()) / 2) + $(window).scrollLeft()); break; } dialog.children('.arrow').attr('class', 'arrow ' + position); dialog.css({ top: top, left: left }); } }, // Is our next tour item visible on screen? If not, scroll to it! dialogVisible: function () { var dialog = $('#tour_dialog'), scrollTop = $(window).scrollTop(), scrollBottom = scrollTop + $(window).height(), elemTop = $(dialog).offset().top, elemBottom = elemTop + $(dialog).height(); if (!((elemBottom >= scrollTop) && (elemTop <= scrollBottom) && (elemBottom <= scrollBottom) && (elemTop >= scrollTop))) { $('html, body').animate({ scrollTop: $(dialog).offset().top - 25 }, 500); } }, setupSelectorsLive: function () { // tour item class allows us to pull an element up the z-index and focus on it // if we pass more than one selector, loop through and add the class to all of them var selArray = TourSteps[this.startFrom].selector.split(","); for (var i = 0; i < selArray.length; i++) { $(selArray[i]).addClass('tour_item'); } // setup triggers for latent actions - use .trigger() to fire these off once the user has complete a custom action in your app var trigger = TourSteps[this.startFrom].waitForTrigger; if (trigger) { $('body').append(''); $(trigger).bind('click.tour', { selector: trigger }, this.resumeClickHandler); } }, // namespaced for tour resume triggers // if a trigger has been activated let's resume the tour where we left off resumeClickHandler: function (e) { $(e.data.selector).unbind('click.tour', this.resumeClickHandler); Tour.showStep(); }, // custom click handler when tour is active. // Check to see if the next action was clicked to advance the tour (nextSelector) tourClickHandler: function (e) { var nextSel = TourSteps[e.data.stop].nextSelector; if (nextSel) { // we only proceed when THIS selector is clicked i.e. #ok_button if ($(e.currentTarget).is(nextSel) || $(nextSel).find(e.currentTarget).length > 0) { Tour.stepComplete(); } } else { // no next selector specified. Any click or action will continue the tour Tour.stepComplete(); } } }; Tour.init(); // kickoff our tour }; }(jQuery, window));