From f0a08a513101539bb7326fb75f74c4cc7a09ad38 Mon Sep 17 00:00:00 2001 From: Gordon Koo Date: Tue, 21 Aug 2012 09:49:20 -0700 Subject: [PATCH] added jquery version --- js/hopscotch_jquery-min.js | 1 + js/hopscotch_jquery.js | 1187 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1188 insertions(+) create mode 100644 js/hopscotch_jquery-min.js create mode 100644 js/hopscotch_jquery.js diff --git a/js/hopscotch_jquery-min.js b/js/hopscotch_jquery-min.js new file mode 100644 index 00000000..700ca62d --- /dev/null +++ b/js/hopscotch_jquery-min.js @@ -0,0 +1 @@ +(function(){var j,c,i,h,f,g="undefined",a=false,d=(typeof window.localStorage!==g),e=document.body.style,b=(typeof e.MozTransition!==g||typeof e.MsTransition!==g||typeof e.webkitTransition!==g||typeof e.OTransition!==g||typeof e.transition!==g);if(window.hopscotch){return}$(window).load(function(){if(a){window.hopscotch.startTour()}});h={getPixelValue:function(l){var k=typeof l;if(k==="number"){return l}if(k==="string"){return parseInt(l,10)}return 0},valOrDefault:function(l,k){return typeof l!==g?l:k},getScrollTop:function(){if(typeof window.pageYOffset!==g){return window.pageYOffset}else{return document.documentElement.scrollTop}},getScrollLeft:function(){if(typeof window.pageXOffset!==g){return window.pageXOffset}else{return document.documentElement.scrollLeft}},evtPreventDefault:function(k){if(k.preventDefault){k.preventDefault()}else{if(event){event.returnValue=false}}},extend:function(l,k){var m;for(m in k){if(k.hasOwnProperty(m)){l[m]=k[m]}}},getStepTarget:function(k){if(typeof k.target==="string"){return $("#"+k.target)}return $(k.target)},setState:function(n,o,p){var k="",l,m;if(d){localStorage.setItem(n,o)}else{if(p){m=new Date();m.setTime(m.getTime()+(p*24*60*60*1000));k="; expires="+m.toGMTString()}document.cookie=n+"="+o+k+"; path=/"}},getState:function(l){var n=l+"=",k=document.cookie.split(";"),m,o;if(d){return localStorage.getItem(l)}else{for(var m=0;m");q.attr({id:s,type:"button",value:r});q.addClass("hopscotch-nav-button");if(s.indexOf("prev")>=0){q.addClass("prev")}else{q.addClass("next")}return q},m=function(r,q,t){var s=t?"hide-all":"hide";if(typeof q===g){q=true}if(q){r.removeClass(s)}else{r.addClass(s)}},n=function(x,s,E){var A,q,y,r,v,w,z,t,C=h.getStepTarget(s)[0],D=x.$element,u=x.$arrowEl,B=h.getPixelValue(s.arrowOffset);E=h.valOrDefault(E,true);A=h.getPixelValue(s.width)||p.bubbleWidth;y=h.valOrDefault(s.padding,p.bubblePadding);bubbleBorder=h.valOrDefault(s.padding,p.bubbleBorder);D.removeClass("bounce-down bounce-up bounce-left bounce-right");r=C.getBoundingClientRect();if(s.orientation==="top"){q=D.height();z=(r.top-q)-p.arrowWidth;t=r.left;w="bounce-down"}else{if(s.orientation==="bottom"){z=r.bottom+p.arrowWidth;t=r.left;w="bounce-up"}else{if(s.orientation==="left"){z=r.top;t=r.left-A-2*y-2*bubbleBorder-p.arrowWidth;w="bounce-right"}else{if(s.orientation==="right"){z=r.top;t=r.right+p.arrowWidth;w="bounce-left"}}}}if(!B){u.css({top:"",left:""})}else{if(s.orientation==="top"||s.orientation==="bottom"){u.css("left",B+"px")}else{if(s.orientation==="left"||s.orientation==="right"){u.css("top",B+"px")}}}t+=h.getPixelValue(s.xOffset);z+=h.getPixelValue(s.yOffset);z+=h.getScrollTop();t+=h.getScrollLeft();if(p.animate){if(!b&&p.animate){D.animate({top:z+"px",left:t+"px"})}else{D.css("top",z+"px");D.css("left",t+"px")}}else{D.css("top",z+"px");D.css("left",t+"px");if(E){v=p.smoothScroll?p.scrollDuration:0;setTimeout(function(){D.addClass(w)},v);setTimeout(function(){D.removeClass(w)},v+2000)}}};this.init=function(){var t=$("
"),r=$("
"),u=$("
"),s=this,v=false,q;this.$element=t;this.$containerEl=r;this.$titleEl=$("

");this.$numberEl=$("");this.$contentEl=$("

");this.$numberEl.attr("id","hopscotch-bubble-number");u.append(this.$titleEl,this.$contentEl).attr("id","hopscotch-bubble-content");r.attr("id","hopscotch-bubble-container").append(this.$numberEl,u);t.attr("id","hopscotch-bubble").addClass("animated").append(r);this.initNavButtons();if(p&&p.showCloseButton){this.initCloseButton()}this.initArrow();window.onresize=function(){if(v||!l){return}v=true;q=setTimeout(function(){n(s,k,false);v=false},200)};$("body").append(t);return this};this.initNavButtons=function(){var q=$("

");this.$prevBtnEl=o("hopscotch-prev",i.prevBtn);this.$nextBtnEl=o("hopscotch-next",i.nextBtn);this.$doneBtnEl=o("hopscotch-done",i.doneBtn);this.$doneBtnEl.addClass("hide");this.$prevBtnEl.click(function(r){window.hopscotch.prevStep()});this.$nextBtnEl.click(function(r){window.hopscotch.nextStep()});this.$doneBtnEl.click(window.hopscotch.endTour);q.attr("id","hopscotch-actions").append(this.$prevBtnEl,this.$nextBtnEl,this.$doneBtnEl);this.buttonsEl=q;this.$containerEl.append(q);return this};this.initCloseButton=function(){var q=$("");q.text(i.closeTooltip).attr({id:"hopscotch-bubble-close",href:"#",title:i.closeTooltip}).click(function(r){window.hopscotch.endTour();h.evtPreventDefault(r)});this.closeBtnEl=q;this.$containerEl.append(q);return this};this.initArrow=function(){var r,q;this.$arrowEl=$("
").attr("id","hopscotch-bubble-arrow-container");q=$("
").addClass("hopscotch-bubble-arrow-border");r=$("
").addClass("hopscotch-bubble-arrow");this.$arrowEl.append(q,r);this.$element.append(this.$arrowEl);return this};this.renderStep=function(s,x,t,u,y){var z=this,r=h.valOrDefault(s.showNextButton,p.showNextButton),q=h.valOrDefault(s.showPrevButton,p.showPrevButton),w,v;k=s;this.setTitle(s.title?s.title:"");this.setContent(s.content?s.content:"");this.setNum(x);this.showPrevButton(this.$prevBtnEl&&q&&(x>0||t>0));this.showNextButton(this.$nextBtnEl&&r&&!u);if(u){this.$doneBtnEl.removeClass("hide")}else{this.$doneBtnEl.addClass("hide")}this.setArrow(s.orientation);w=h.getPixelValue(s.width)||p.bubbleWidth;v=h.valOrDefault(s.padding,p.bubblePadding);this.$containerEl.css({width:w+"px",padding:v+"px"});if(s.orientation==="top"){setTimeout(function(){n(z,s);if(y){y()}},5)}else{n(this,s);if(y){y()}}return this};this.setTitle=function(q){if(q){this.$titleEl.html(q).removeClass("hide")}else{this.$titleEl.addClass("hide")}return this};this.setContent=function(q){if(q){this.$contentEl.html(q).removeClass("hide")}else{this.$contentEl.addClass("hide")}return this};this.setNum=function(q){if(i.stepNums&&q0)?B[x]:B},y=function(){var B=r.steps[m].length;if(x0){--x;return true}else{if(m>0){B=r.steps[--m].length;if(B){x=B-1}else{x=undefined}return true}}return false},k=function(){return r.steps[m].length>0},v=function(E,B){var D=f[E],C=0;len=D.length;for(;C1){q=parseInt(C[0],10);s=parseInt(C[1],10)}else{q=parseInt(q,10)}if(tourPair.length>2&&tourPair[2]==="mp"){if(s&&s0){s=0}else{s=undefined}}}}}B=p();B.showPrevButton(n.showPrevButton,true);B.showNextButton(n.showNextButton,true);return this};this.startTour=function(G,F){var C,E,D,B;if(!r){throw"Need to load a tour before you start it!"}if(document.readyState!=="complete"){a=true;return}if(typeof G!==g){m=G;x=F}else{if(r.id===A&&typeof q!==g){m=q;x=s;E=z();if(!h.getStepTarget(E)){u();E=z();if(!h.getStepTarget(E)){this.endTour(false);return}}}else{m=0}}if(!x&&k()){x=0}if(m===0&&!x){v("start",[r.id])}this.showStep(m,x);C=p().show();if(n.animate){C.initAnimate()}this.isActive=true;return this};this.showStep=function(F,D){var I=r.steps,G=I[F],E=I.length,C=r.id+":"+F,B=p(),H;m=F;x=D;if(typeof D!==g&&k()){G=G[D];C+="-"+D}H=(F===E-1)||(D>=G.length-1);B.renderStep(G,F,D,H,t);if(G.multiPage){C+=":mp"}h.setState(n.cookieName,C,1);return this};this.prevStep=function(){var C=z(),B=false;v("prev",[r.id,m]);if(C.onPrev){C.onPrev()}if(n.skipIfNoElement){while(!B&&u()){C=z();B=h.getStepTarget(C)}if(!B){this.endTour()}}else{if(u()){C=z();if(!h.getStepTarget(C)){v("error",[r.id,m]);return}}}this.showStep(m,x);return this};this.nextStep=function(){var C=z(),B=false;v("next",[r.id,m]);if(C.onNext){C.onNext()}if(n.skipIfNoElement){while(!B&&y()){C=z();B=h.getStepTarget(C)}if(!B){this.endTour()}}else{if(y()){C=z();if(!h.getStepTarget(C)){v("error",[r.id,m]);this.endTour();return}}}this.showStep(m,x);return this};this.endTour=function(C){var B=p();C=h.valOrDefault(C,true);m=0;x=undefined;q=undefined;B.hide();if(C){h.clearState(n.cookieName)}this.isActive=false;v("end",[r.id]);hopscotch.removeCallbacks(true);return this};this.getCurrStepNum=function(){return m};this.getCurrSubstepNum=function(){return x};this.hasTakenTour=function(B){if(d){h.getState(n.cookieName+"_history")}return false};this.setHasTakenTour=function(C){var B;if(d&&!this.hasTakenTour(C)){B=h.getState(n.cookieName+"_history");if(B){B+=";"+C}else{B=C}}};this.clearHasTakenTour=function(H){var G,F,C,B,D=n.cookieName+"_history",E=false;if(d){G=h.getState(D);if(G){F=G.split(";");for(C=0,B=F.length;C 1 bubble at a time? gahhhhhhhhh + * "center" option (for block-level elements that span width of document) + * + * TODO: + * ===== + * test css conflicts on different sites + * improve auto-scrolling? + * create hopscotch-jquery.js and hopscotch-yui.js?? blaarlkghiidsfffzp\09u93}%^*(!! + * + * position screws up when you align it with a position:fixed element + * delay for displaying a step + * + * onShow, onHide callbacks? + * support horizontal smooth scroll???????? + * + * in addition to targetId, do we want to support specifying targetEl directly? + * + * flag to see if user has already taken a tour? + * + * http://daneden.me/animate/ for bounce animation + * + * multiple start/end callbacks + * + */ + +(function() { + var Hopscotch, + HopscotchBubble, + HopscotchI18N, + utils, + callbacks, + undefinedStr = 'undefined', + waitingToStart = false, // is a tour waiting for the document to finish + // loading so that it can start? + hasLocalStorage = (typeof window.localStorage !== undefinedStr), + docStyle = document.body.style, + hasCssTransitions = (typeof docStyle.MozTransition !== undefinedStr || + typeof docStyle.MsTransition !== undefinedStr || + typeof docStyle.webkitTransition !== undefinedStr || + typeof docStyle.OTransition !== undefinedStr || + typeof docStyle.transition !== undefinedStr); + + if (window.hopscotch) { + // Hopscotch already exists. + return; + } + + $(window).load(function() { + if (waitingToStart) { + window.hopscotch.startTour(); + } + }); + + /** + * utils + * ===== + * A set of utility functions, mostly for standardizing to manipulate + * and extract information from the DOM. Basically these are things I + * would normally use jQuery for, but I don't want to require it for + * this framework. + */ + utils = { + getPixelValue: function(val) { + var valType = typeof val; + if (valType === 'number') { return val; } + if (valType === 'string') { return parseInt(val, 10); } + return 0; + }, + + // Inspired by Python... + valOrDefault: function(val, valDefault) { + return typeof val !== undefinedStr ? val : valDefault; + }, + + getScrollTop: function() { + if (typeof window.pageYOffset !== undefinedStr) { + return window.pageYOffset; + } + else { + // Most likely IE <=8, which doesn't support pageYOffset + return document.documentElement.scrollTop; + } + }, + + getScrollLeft: function() { + if (typeof window.pageXOffset !== undefinedStr) { + return window.pageXOffset; + } + else { + // Most likely IE <=8, which doesn't support pageXOffset + return document.documentElement.scrollLeft; + } + }, + + evtPreventDefault: function(evt) { + if (evt.preventDefault) { + evt.preventDefault(); + } + else if (event) { + event.returnValue = false; + } + }, + + extend: function(obj1, obj2) { + var prop; + for (prop in obj2) { + if (obj2.hasOwnProperty(prop)) { + obj1[prop] = obj2[prop]; + } + } + }, + + getStepTarget: function(step) { + if (typeof step.target === 'string') { + return $('#' + step.target); + } + return $(step.target); + }, + + // Tour session persistence for multi-page tours. Uses HTML5 localStorage if available, then + // falls back to using cookies. + // + // The following cookie-related logic is borrowed from: + // http://www.quirksmode.org/js/cookies.html + + setState: function(name,value,days) { + var expires = '', + userDataName, + date; + + if (hasLocalStorage) { + localStorage.setItem(name, value); + } + else { + if (days) { + date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + expires = "; expires="+date.toGMTString(); + } + document.cookie = name+"="+value+expires+"; path=/"; + } + }, + + getState: function(name) { + var nameEQ = name + "=", + ca = document.cookie.split(';'), + i, + c; + + if (hasLocalStorage) { + return localStorage.getItem(name); + } + else { + for(var i=0;i < ca.length;i++) { + c = ca[i]; + while (c.charAt(0)===' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length); + } + return null; + } + }, + + clearState: function(name) { + if (hasLocalStorage) { + localStorage.removeItem(name); + } + else { + this.setState(name,"",-1); + } + } + }; + + callbacks = { + next: [], + prev: [], + start: [], + end: [], + error: [] + }; + + HopscotchI18N = { + stepNums: null, + nextBtn: 'Next', + prevBtn: 'Back', + doneBtn: 'Done', + closeTooltip: 'Close' + }; + + HopscotchBubble = function(opt) { + var isShowing = false, + currStep, + + createButton = function(id, text) { + var $btnEl = $(''); + $btnEl.attr({ + id: id, + type: 'button', + value: text + }); + $btnEl.addClass('hopscotch-nav-button'); + + if (id.indexOf('prev') >= 0) { + $btnEl.addClass('prev'); + } + else { + $btnEl.addClass('next'); + } + return $btnEl; + }, + + showButton = function($btnEl, show, permanent) { + // permanent is a flag that indicates we should never show the button + var classname = permanent ? 'hide-all' : 'hide'; + + if (typeof show === undefinedStr) { + show = true; + } + + if (show) { $btnEl.removeClass(classname); } + else { $btnEl.addClass(classname); } + }, + + /** + * setPosition + * =========== + * Sets the position of the bubble using the bounding rectangle of the + * target element and the orientation and offset information specified by + * the step JSON. + */ + setPosition = function(bubble, step, bounce) { + var bubbleWidth, + bubbleHeight, + bubblePadding, + boundingRect, + bounceDelay, + bounceDirection, + top, + left, + targetEl = utils.getStepTarget(step)[0], + $el = bubble.$element, + $arrowEl = bubble.$arrowEl, + arrowOffset = utils.getPixelValue(step.arrowOffset); + + bounce = utils.valOrDefault(bounce, true); + bubbleWidth = utils.getPixelValue(step.width) || opt.bubbleWidth; + bubblePadding = utils.valOrDefault(step.padding, opt.bubblePadding); + bubbleBorder = utils.valOrDefault(step.padding, opt.bubbleBorder); + + $el.removeClass('bounce-down bounce-up bounce-left bounce-right'); + + // SET POSITION + boundingRect = targetEl.getBoundingClientRect(); + if (step.orientation === 'top') { + bubbleHeight = $el.height(); + top = (boundingRect.top - bubbleHeight) - opt.arrowWidth; + left = boundingRect.left; + bounceDirection = 'bounce-down'; + } + else if (step.orientation === 'bottom') { + top = boundingRect.bottom + opt.arrowWidth; + left = boundingRect.left; + bounceDirection = 'bounce-up'; + } + else if (step.orientation === 'left') { + top = boundingRect.top; + left = boundingRect.left - bubbleWidth - 2*bubblePadding - 2*bubbleBorder - opt.arrowWidth; + bounceDirection = 'bounce-right'; + } + else if (step.orientation === 'right') { + top = boundingRect.top; + left = boundingRect.right + opt.arrowWidth; + bounceDirection = 'bounce-left'; + } + + // SET (OR RESET) ARROW OFFSETS + if (!arrowOffset) { + $arrowEl.css({ + top: '', + left: '' + }); + } + else if (step.orientation === 'top' || step.orientation === 'bottom') { + $arrowEl.css('left', arrowOffset + 'px'); + } + else if (step.orientation === 'left' || step.orientation === 'right') { + $arrowEl.css('top', arrowOffset + 'px'); + } + + // SET OFFSETS + left += utils.getPixelValue(step.xOffset); + top += utils.getPixelValue(step.yOffset); + + // ADJUST TOP FOR SCROLL POSITION + top += utils.getScrollTop(); + left += utils.getScrollLeft(); + + if (opt.animate) { + if (!hasCssTransitions && opt.animate) { + $el.animate({ + top: top + 'px', + left: left + 'px' + }); + } + else { // hasCssTransitions || !opt.animate + $el.css('top', top + 'px'); + $el.css('left', left + 'px'); + } + } + else { + $el.css('top', top + 'px'); + $el.css('left', left + 'px'); + + // Do the bouncing effect + if (bounce) { + bounceDelay = opt.smoothScroll ? opt.scrollDuration : 0; + + setTimeout(function() { + $el.addClass(bounceDirection); + }, bounceDelay); + // Then remove it + setTimeout(function() { + $el.removeClass(bounceDirection); + }, bounceDelay + 2000); // bounce lasts 2 seconds + } + } + }; + + this.init = function() { + var $el = $('
'), + $containerEl = $('
'), + $bubbleContentEl = $('
'), + self = this, + resizeCooldown = false, // for updating after window resize + winResizeTimeout; + + this.$element = $el; + this.$containerEl = $containerEl; + this.$titleEl = $('

'); + this.$numberEl = $(''); + this.$contentEl = $('

'); + + this.$numberEl.attr('id', 'hopscotch-bubble-number'); + + $bubbleContentEl.append(this.$titleEl, this.$contentEl) + .attr('id', 'hopscotch-bubble-content'); + + $containerEl.attr('id', 'hopscotch-bubble-container') + .append(this.$numberEl, $bubbleContentEl); + + $el.attr('id', 'hopscotch-bubble') + .addClass('animated') + .append($containerEl); + + this.initNavButtons(); + + if (opt && opt.showCloseButton) { + this.initCloseButton(); + } + + this.initArrow(); + + // Not pretty, but IE doesn't support Function.bind(), so I'm + // relying on closures to keep a handle of "this". + // Reset position of bubble when window is resized + window.onresize = function() { + if (resizeCooldown || !isShowing) { + return; + } + resizeCooldown = true; + winResizeTimeout = setTimeout(function() { + setPosition(self, currStep, false); + resizeCooldown = false; + }, 200); + }; + + $('body').append($el); + return this; + }; + + this.initNavButtons = function() { + var $buttonsEl = $('

'); + + this.$prevBtnEl = createButton('hopscotch-prev', HopscotchI18N.prevBtn); + this.$nextBtnEl = createButton('hopscotch-next', HopscotchI18N.nextBtn); + this.$doneBtnEl = createButton('hopscotch-done', HopscotchI18N.doneBtn); + this.$doneBtnEl.addClass('hide'); + + + // Attach click listeners + this.$prevBtnEl.click(function(evt) { + window.hopscotch.prevStep(); + }); + this.$nextBtnEl.click(function(evt) { + window.hopscotch.nextStep(); + }); + this.$doneBtnEl.click(window.hopscotch.endTour); + + $buttonsEl.attr('id', 'hopscotch-actions') + .append(this.$prevBtnEl, + this.$nextBtnEl, + this.$doneBtnEl); + + this.buttonsEl = $buttonsEl; + + this.$containerEl.append($buttonsEl); + return this; + }; + + this.initCloseButton = function() { + var $closeBtnEl = $(''); + + $closeBtnEl.text(HopscotchI18N.closeTooltip) + .attr({ + id: 'hopscotch-bubble-close', + href: '#', + title: HopscotchI18N.closeTooltip + }) + .click(function(evt) { + window.hopscotch.endTour(); + utils.evtPreventDefault(evt); + }); + + this.closeBtnEl = $closeBtnEl; + this.$containerEl.append($closeBtnEl); + return this; + }; + + this.initArrow = function() { + var $arrowEl, + $arrowBorderEl; + + this.$arrowEl = $('
').attr('id', 'hopscotch-bubble-arrow-container'); + + $arrowBorderEl = $('
').addClass('hopscotch-bubble-arrow-border'); + + $arrowEl = $('
').addClass('hopscotch-bubble-arrow'); + + this.$arrowEl.append($arrowBorderEl, $arrowEl); + + this.$element.append(this.$arrowEl); + return this; + }; + + this.renderStep = function(step, idx, subIdx, isLast, callback) { + var self = this, + showNext = utils.valOrDefault(step.showNextButton, opt.showNextButton), + showPrev = utils.valOrDefault(step.showPrevButton, opt.showPrevButton), + bubbleWidth, + bubblePadding; + + currStep = step; + this.setTitle(step.title ? step.title : ''); + this.setContent(step.content ? step.content : ''); + this.setNum(idx); + + this.showPrevButton(this.$prevBtnEl && showPrev && (idx > 0 || subIdx > 0)); + this.showNextButton(this.$nextBtnEl && showNext && !isLast); + if (isLast) { + this.$doneBtnEl.removeClass('hide'); + } + else { + this.$doneBtnEl.addClass('hide'); + } + + this.setArrow(step.orientation); + + // Set dimensions + bubbleWidth = utils.getPixelValue(step.width) || opt.bubbleWidth; + bubblePadding = utils.valOrDefault(step.padding, opt.bubblePadding); + this.$containerEl.css({ + width: bubbleWidth + 'px', + padding: bubblePadding + 'px' + }); + + if (step.orientation === 'top') { + // Timeout to get correct height of bubble for positioning. + setTimeout(function() { + setPosition(self, step); + if (callback) { callback(); } + }, 5); + } + else { + // Don't care about height for the other orientations. + setPosition(this, step); + if (callback) { callback(); } + } + + return this; + }; + + this.setTitle = function(titleStr) { + // CAREFUL!! Using $.html(), so don't use any user-generated + // content here. (or if you must, escape it first) + if (titleStr) { + this.$titleEl.html(titleStr) + .removeClass('hide'); + } + else { + this.$titleEl.addClass('hide'); + } + return this; + }; + + this.setContent = function(contentStr) { + // CAREFUL!! Using $.html(), so don't use any user-generated + // content here. (or if you must, escape it first) + if (contentStr) { + this.$contentEl.html(contentStr) + .removeClass('hide'); + } + else { + this.$contentEl.addClass('hide'); + } + return this; + }; + + this.setNum = function(idx) { + if (HopscotchI18N.stepNums && idx < HopscotchI18N.stepNums.length) { + idx = HopscotchI18N.stepNums[idx]; + } + else { + idx = idx + 1; + } + this.$numberEl.html(idx); + }; + + this.setArrow = function(orientation) { + // Whatever the orientation is, we want to arrow to appear + // "opposite" of the orientation. E.g., a top orientation + // requires a bottom arrow. + if (orientation === 'top') { + this.$arrowEl.removeClass() + .addClass('down'); + } + else if (orientation === 'bottom') { + this.$arrowEl.removeClass() + .addClass('up'); + } + else if (orientation === 'left') { + this.$arrowEl.removeClass() + .addClass('right'); + } + else if (orientation === 'right') { + this.$arrowEl.removeClass() + .addClass('left'); + } + }; + + this.show = function() { + var self = this; + if (opt.animate) { + setTimeout(function() { + self.$element.addClass('animate'); + }, 50); + } + this.$element.removeClass('hide'); + isShowing = true; + return this; + }; + + this.hide = function() { + this.$element.addClass('hide') + .removeClass('animate'); + isShowing = false; + return this; + }; + + this.showPrevButton = function(show, permanent) { + showButton(this.$prevBtnEl, show, permanent); + }; + + this.showNextButton = function(show, permanent) { + showButton(this.$nextBtnEl, show, permanent); + }; + + this.showCloseButton = function(show, permanent) { + showButton(this.closeBtnEl, show, permanent); + }; + + + /** + * initAnimate + * =========== + * This function exists due to how Chrome handles initial CSS transitions. + * Most other browsers will not animate a transition until the element + * exists on the page. Chrome treats DOM elements as starting from the + * (0, 0) position, and will animate from the upper left corner on creation + * of the DOM element. (e.g., if you create a new DOM element using + * Javascript and specify CSS top: 100px, left: 50px, then append the + * DOM element to the document.body, it will create it at 0, 0 and then + * animate it to 50, 100) + * + * Solution is to add the animate class (which defines our transition) + * only after the element is created. + */ + this.initAnimate = function() { + var self = this; + setTimeout(function() { + self.$element.addClass('animate'); + }, 50); + }; + + this.removeAnimate = function() { + this.$element.removeClass('animate'); + }; + + this.init(); + }; + + Hopscotch = function(initOptions) { + var bubble, + opt, + currTour, + currStepNum, + currSubstepNum, + cookieTourId, + cookieTourStep, + cookieTourSubstep, + _configure, + + /** + * getBubble + * ========== + * Retrieves the "singleton" bubble div or creates it if it doesn't + * exist yet. + */ + getBubble = function() { + if (!bubble) { + bubble = new HopscotchBubble(opt); + } + return bubble; + }, + + getCurrStep = function() { + var step = currTour.steps[currStepNum]; + + return (step.length > 0) ? step[currSubstepNum] : step; + }, + + /** + * incrementStep + * ============= + * Sets current step num and substep num to the next step in the tour. + * Returns true if successful, false if not. + */ + incrementStep = function() { + var numSubsteps = currTour.steps[currStepNum].length; + if (currSubstepNum < numSubsteps-1) { + ++currSubstepNum; + return true; + } + else if (currStepNum < currTour.steps.length-1) { + ++currStepNum; + currSubstepNum = isInMultiPartStep() ? 0 : undefined; + return true; + } + return false; + }, + + /** + * decrementStep + * ============= + * Sets current step num and substep num to the previous step in the tour. + * Returns true if successful, false if not. + */ + decrementStep = function() { + var numPrevSubsteps; + if (currSubstepNum > 0) { + --currSubstepNum; + return true; + } + else if (currStepNum > 0) { + numPrevSubsteps = currTour.steps[--currStepNum].length; + if (numPrevSubsteps) { + currSubstepNum = numPrevSubsteps-1; + } + else { + currSubstepNum = undefined; + } + return true; + } + return false; + }, + + isInMultiPartStep = function() { + return currTour.steps[currStepNum].length > 0; + }, + + invokeCallbacks = function(evtType, args) { + var cbArr = callbacks[evtType], + i = 0; + len = cbArr.length; + + for (; i 1) { + cookieTourStep = parseInt(stepPair[0], 10); + cookieTourSubstep = parseInt(stepPair[1], 10); + } + else { + cookieTourStep = parseInt(cookieTourStep, 10); + } + + // Check for multipage flag + if (tourPair.length > 2 && tourPair[2] === 'mp') { + // Increment cookie step + if (cookieTourSubstep && cookieTourSubstep < currTour.steps[cookieTourStep].length-1) { + ++cookieTourSubstep; + } + else if (cookieTourStep < currTour.steps.length-1) { + ++cookieTourStep; + if (currTour.steps[cookieTourStep].length > 0) { + cookieTourSubstep = 0; + } + else { + cookieTourSubstep = undefined; + } + } + } + } + + // Initialize whether to show or hide nav buttons + bubble = getBubble(); + bubble.showPrevButton(opt.showPrevButton, true); + bubble.showNextButton(opt.showNextButton, true); + return this; + }; + + this.startTour = function(stepNum, substepNum) { + var bubble, + step, + i, + len; + + if (!currTour) { + throw "Need to load a tour before you start it!"; + } + + if (document.readyState !== 'complete') { + waitingToStart = true; + return; + } + + if (typeof stepNum !== undefinedStr) { + currStepNum = stepNum; + currSubstepNum = substepNum; + } + + // Check if we are resuming state. + else if (currTour.id === cookieTourId && typeof cookieTourStep !== undefinedStr) { + currStepNum = cookieTourStep; + currSubstepNum = cookieTourSubstep; + step = getCurrStep(); + if (!utils.getStepTarget(step)) { + decrementStep(); + step = getCurrStep(); + // May have just refreshed the page. Previous step should work. (but don't change cookie) + if (!utils.getStepTarget(step)) { + // Previous target doesn't exist either. The user may have just + // clicked on a link that wasn't part of the tour. Let's just "end" + // the tour and depend on the cookie to pick the user back up where + // she left off. + this.endTour(false); + return; + } + } + } + else { + currStepNum = 0; + } + + if (!currSubstepNum && isInMultiPartStep()) { + // Multi-part step + currSubstepNum = 0; + } + + if (currStepNum === 0 && !currSubstepNum) { + invokeCallbacks('start', [currTour.id]) + } + + this.showStep(currStepNum, currSubstepNum); + bubble = getBubble().show(); + + if (opt.animate) { + bubble.initAnimate(); + } + this.isActive = true; + return this; + }; + + this.showStep = function(stepIdx, substepIdx) { + var tourSteps = currTour.steps, + step = tourSteps[stepIdx], + numTourSteps = tourSteps.length, + cookieVal = currTour.id + ':' + stepIdx, + bubble = getBubble(), + isLast; + + // Update bubble for current step + currStepNum = stepIdx; + currSubstepNum = substepIdx; + + if (typeof substepIdx !== undefinedStr && isInMultiPartStep()) { + step = step[substepIdx]; + cookieVal += '-' + substepIdx; + } + + isLast = (stepIdx === numTourSteps - 1) || (substepIdx >= step.length - 1); + bubble.renderStep(step, stepIdx, substepIdx, isLast, adjustWindowScroll); + + if (step.multiPage) { + cookieVal += ':mp'; + } + + utils.setState(opt.cookieName, cookieVal, 1); + return this; + }; + + this.prevStep = function() { + var step = getCurrStep(), + foundTarget = false; + + invokeCallbacks('prev', [currTour.id, currStepNum]); + if (step.onPrev) { + step.onPrev(); + } + + if (opt.skipIfNoElement) { + // decrement step until we find a target or until we reach beginning + while (!foundTarget && decrementStep()) { + step = getCurrStep(); + foundTarget = utils.getStepTarget(step); + } + if (!foundTarget) { + this.endTour(); + } + } + + else if (decrementStep()) { + // only try decrementing once, and invoke error callback if no target + // is found + step = getCurrStep(); + if (!utils.getStepTarget(step)) { + invokeCallbacks('error', [currTour.id, currStepNum]); + return; + } + } + + this.showStep(currStepNum, currSubstepNum); + return this; + }; + + this.nextStep = function() { + var step = getCurrStep(), + foundTarget = false; + + // invoke Next button callbacks + invokeCallbacks('next', [currTour.id, currStepNum]); + + if (step.onNext) { + step.onNext(); + } + + if (opt.skipIfNoElement) { + // decrement step until we find a target or until we reach beginning + while (!foundTarget && incrementStep()) { + step = getCurrStep(); + foundTarget = utils.getStepTarget(step); + } + if (!foundTarget) { + this.endTour(); + } + } + + else if (incrementStep()) { + // only try decrementing once, and invoke error callback if no target + // is found + step = getCurrStep(); + if (!utils.getStepTarget(step)) { + invokeCallbacks('error', [currTour.id, currStepNum]); + this.endTour(); + return; + } + } + this.showStep(currStepNum, currSubstepNum); + + return this; + }; + + /** + * endTour + * ========== + * Cancels out of an active tour. No state is preserved. + */ + this.endTour = function(clearCookie) { + var bubble = getBubble(); + clearCookie = utils.valOrDefault(clearCookie, true); + currStepNum = 0; + currSubstepNum = undefined; + cookieTourStep = undefined; + + bubble.hide(); + if (clearCookie) { + utils.clearState(opt.cookieName); + } + this.isActive = false; + + invokeCallbacks('end', [currTour.id]); + + hopscotch.removeCallbacks(true); + + return this; + }; + + this.getCurrStepNum = function() { + return currStepNum; + }; + + this.getCurrSubstepNum = function() { + return currSubstepNum; + }; + + this.hasTakenTour = function(tourId) { + if (hasLocalStorage) { + utils.getState(opt.cookieName + '_history'); + } + return false; + }; + + this.setHasTakenTour = function(tourId) { + var history; + if (hasLocalStorage && !this.hasTakenTour(tourId)) { + history = utils.getState(opt.cookieName + '_history'); + if (history) { + history += ';'+tourId; + } + else { + history = tourId; + } + } + }; + + this.clearHasTakenTour = function(tourId) { + var history, + tourIds, + i, + len, + historyName = opt.cookieName + '_history', + found = false; + + if (hasLocalStorage) { + history = utils.getState(historyName); + if (history) { + tourIds = history.split(';'); + for (i=0, len=tourIds.length; i - Provide a list of strings to be shown as + * the step number, based on index of array. Unicode + * characters are supported. (e.g., ['一', + * '二', '三']) If there are more steps + * than provided numbers, Arabic numerals + * ('4', '5', '6', etc.) will be used as default. + * + * isTourOptions: This is a flag for the purpose of removing tour-specific + * callbacks once a tour ends. This is only used + * internally. + */ + _configure = function(options, isTourOptions) { + var bubble; + + if (!opt) { + opt = {}; + } + + utils.extend(opt, options); + opt.animate = utils.valOrDefault(opt.animate, false); + opt.smoothScroll = utils.valOrDefault(opt.smoothScroll, true); + opt.scrollDuration = utils.valOrDefault(opt.scrollDuration, 1000); + opt.scrollTopMargin = utils.valOrDefault(opt.scrollTopMargin, 200); + opt.showCloseButton = utils.valOrDefault(opt.showCloseButton, true); + opt.showPrevButton = utils.valOrDefault(opt.showPrevButton, false); + opt.showNextButton = utils.valOrDefault(opt.showNextButton, true); + opt.bubbleWidth = utils.valOrDefault(opt.bubbleWidth, 280); + opt.bubblePadding = utils.valOrDefault(opt.bubblePadding, 15); + opt.bubbleBorder = utils.valOrDefault(opt.bubbleBorder, 6); + opt.arrowWidth = utils.valOrDefault(opt.arrowWidth, 20); + opt.skipIfNoElement = utils.valOrDefault(opt.skipIfNoElement, false); + opt.cookieName = utils.valOrDefault(opt.cookieName, 'hopscotch.tour.state'); + + if (options) { + utils.extend(HopscotchI18N, options.i18n); + } + + this.addCallback('next', options.onNext, isTourOptions); + this.addCallback('prev', options.onPrev, isTourOptions); + this.addCallback('start', options.onStart, isTourOptions); + this.addCallback('end', options.onEnd, isTourOptions); + this.addCallback('error', options.onError, isTourOptions); + + bubble = getBubble(); + + if (opt.animate) { + bubble.initAnimate(); + } + else { + bubble.removeAnimate(); + } + + bubble.showPrevButton(opt.showPrevButton, true); + bubble.showNextButton(opt.showNextButton, true); + bubble.showCloseButton(opt.showCloseButton, true); + return this; + }; + + /** + * configure + * ========= + * Just a wrapper for _configure, to make sure developers don't try and set + * isTourOptions. + */ + this.configure = function(options) { + _configure.call(this, options, false); + }; + + this.init(initOptions); + }; + + window.hopscotch = new Hopscotch(); +}());