From ef83ec745f32b7c3d4752a05bd3ca803fda38ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Jer=C3=B3nimo?= Date: Fri, 6 Jun 2014 17:09:43 +0100 Subject: [PATCH] add hopscotch --- ajax/libs/hopscotch/0.2.0/hopscotch.js | 2295 ++++++++++++++++++++ ajax/libs/hopscotch/0.2.0/hopscotch.min.js | 17 + ajax/libs/hopscotch/package.json | 41 + 3 files changed, 2353 insertions(+) create mode 100644 ajax/libs/hopscotch/0.2.0/hopscotch.js create mode 100644 ajax/libs/hopscotch/0.2.0/hopscotch.min.js create mode 100644 ajax/libs/hopscotch/package.json diff --git a/ajax/libs/hopscotch/0.2.0/hopscotch.js b/ajax/libs/hopscotch/0.2.0/hopscotch.js new file mode 100644 index 000000000..68a21ff73 --- /dev/null +++ b/ajax/libs/hopscotch/0.2.0/hopscotch.js @@ -0,0 +1,2295 @@ +/**! hopscotch - v0.2.0 +* +* Copyright 2014 LinkedIn Corp. All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* 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(context, namespace) { + var Hopscotch, + HopscotchBubble, + HopscotchCalloutManager, + HopscotchI18N, + customI18N, + customRenderer, + customEscape, + templateToUse = 'bubble_default', + Sizzle = window.Sizzle || null, + utils, + callbacks, + helpers, + winLoadHandler, + defaultOpts, + winHopscotch = context[namespace], + undefinedStr = 'undefined', + waitingToStart = false, // is a tour waiting for the document to finish + // loading so that it can start? + hasJquery = (typeof window.jQuery !== undefinedStr), + hasSessionStorage = false, + document = window.document; + + // If cookies are disabled, accessing sessionStorage can throw an error. + try { + hasSessionStorage = (typeof window.sessionStorage !== undefinedStr); + } catch (err) {} + + defaultOpts = { + smoothScroll: true, + scrollDuration: 1000, + scrollTopMargin: 200, + showCloseButton: true, + showPrevButton: false, + showNextButton: true, + bubbleWidth: 280, + bubblePadding: 15, + arrowWidth: 20, + skipIfNoElement: true, + cookieName: 'hopscotch.tour.state' + }; + + if (winHopscotch) { + // Hopscotch already exists. + return; + } + + if (!Array.isArray) { + Array.isArray = function(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + /** + * Called when the page is done loading. + * + * @private + */ + winLoadHandler = function() { + if (waitingToStart) { + winHopscotch.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. + * + * @private + */ + utils = { + /** + * addClass + * ======== + * Adds one or more classes to a DOM element. + * + * @private + */ + addClass: function(domEl, classToAdd) { + var domClasses, + classToAddArr, + setClass, + i, + len; + + if (!domEl.className) { + domEl.className = classToAdd; + } + else { + classToAddArr = classToAdd.split(/\s+/); + domClasses = ' ' + domEl.className + ' '; + for (i = 0, len = classToAddArr.length; i < len; ++i) { + if (domClasses.indexOf(' ' + classToAddArr[i] + ' ') < 0) { + domClasses += classToAddArr[i] + ' '; + } + } + domEl.className = domClasses.replace(/^\s+|\s+$/g,''); + } + }, + + /** + * removeClass + * =========== + * Remove one or more classes from a DOM element. + * + * @private + */ + removeClass: function(domEl, classToRemove) { + var domClasses, + classToRemoveArr, + currClass, + i, + len; + + classToRemoveArr = classToRemove.split(/\s+/); + domClasses = ' ' + domEl.className + ' '; + for (i = 0, len = classToRemoveArr.length; i < len; ++i) { + domClasses = domClasses.replace(' ' + classToRemoveArr[i] + ' ', ' '); + } + domEl.className = domClasses.replace(/^\s+|\s+$/g,''); + }, + + /** + * hasClass + * ======== + * Determine if a given DOM element has a class. + */ + hasClass: function(domEl, classToCheck){ + var classes; + + if(!domEl.className){ return false; } + classes = ' ' + domEl.className + ' '; + return (classes.indexOf(' ' + classToCheck + ' ') !== -1); + }, + + /** + * @private + */ + getPixelValue: function(val) { + var valType = typeof val; + if (valType === 'number') { return val; } + if (valType === 'string') { return parseInt(val, 10); } + return 0; + }, + + /** + * Inspired by Python... returns val if it's defined, otherwise returns the default. + * + * @private + */ + valOrDefault: function(val, valDefault) { + return typeof val !== undefinedStr ? val : valDefault; + }, + + /** + * Invokes a single callback represented by an array. + * Example input: ["my_fn", "arg1", 2, "arg3"] + * @private + */ + invokeCallbackArrayHelper: function(arr) { + // Logic for a single callback + var fn; + if (Array.isArray(arr)) { + fn = helpers[arr[0]]; + if (typeof fn === 'function') { + return fn.apply(this, arr.slice(1)); + } + } + }, + + /** + * Invokes one or more callbacks. Array should have at most one level of nesting. + * Example input: + * ["my_fn", "arg1", 2, "arg3"] + * [["my_fn_1", "arg1", "arg2"], ["my_fn_2", "arg2-1", "arg2-2"]] + * [["my_fn_1", "arg1", "arg2"], function() { ... }] + * @private + */ + invokeCallbackArray: function(arr) { + var i, len; + + if (Array.isArray(arr)) { + if (typeof arr[0] === 'string') { + // Assume there are no nested arrays. This is the one and only callback. + return utils.invokeCallbackArrayHelper(arr); + } + else { // assume an array + for (i = 0, len = arr.length; i < len; ++i) { + utils.invokeCallback(arr[i]); + } + } + } + }, + + /** + * Helper function for invoking a callback, whether defined as a function literal + * or an array that references a registered helper function. + * @private + */ + invokeCallback: function(cb) { + if (typeof cb === 'function') { + return cb(); + } + if (typeof cb === 'string' && helpers[cb]) { // name of a helper + return helpers[cb](); + } + else { // assuming array + return utils.invokeCallbackArray(cb); + } + }, + + /** + * If stepCb (the step-specific helper callback) is passed in, then invoke + * it first. Then invoke tour-wide helper. + * + * @private + */ + invokeEventCallbacks: function(evtType, stepCb) { + var cbArr = callbacks[evtType], + callback, + fn, + i, + len; + + if (stepCb) { + return this.invokeCallback(stepCb); + } + + for (i=0, len=cbArr.length; i 0)), + showNext: utils.valOrDefault(step.showNextButton, this.opt.showNextButton), + showCTA: utils.valOrDefault((step.showCTAButton && step.ctaLabel), false), + ctaLabel: step.ctaLabel, + showClose: utils.valOrDefault(this.opt.showCloseButton, true) + }, + step:{ + num: idx, + isLast: utils.valOrDefault(isLast, false), + title: (step.title || ''), + content: (step.content || ''), + placement: step.placement, + padding: utils.valOrDefault(step.padding, this.opt.bubblePadding), + width: utils.getPixelValue(step.width) || this.opt.bubbleWidth, + customData: (step.customData || {}) + }, + tour:{ + isTour: this.opt.isTourBubble, + numSteps: totalSteps, + unsafe: utils.valOrDefault(unsafe, false), + customData: (customTourData || {}) + } + }; + + // Render the bubble's content. + // Use tour renderer if available, then the global customRenderer if defined. + if(typeof tourSpecificRenderer === 'function'){ + el.innerHTML = tourSpecificRenderer(opts); + } + else if(typeof tourSpecificRenderer === 'string'){ + if(!hopscotch.templates || (typeof hopscotch.templates[tourSpecificRenderer] !== 'function')){ + throw 'Bubble rendering failed - template "' + tourSpecificRenderer + '" is not a function.'; + } + el.innerHTML = hopscotch.templates[tourSpecificRenderer](opts); + } + else if(customRenderer){ + el.innerHTML = customRenderer(opts); + } + else{ + if(!hopscotch.templates || (typeof hopscotch.templates[templateToUse] !== 'function')){ + throw 'Bubble rendering failed - template "' + templateToUse + '" is not a function.'; + } + el.innerHTML = hopscotch.templates[templateToUse](opts); + } + + // Find arrow among new child elements. + children = el.children; + numChildren = children.length; + for (i = 0; i < numChildren; i++){ + node = children[i]; + + if(utils.hasClass(node, 'hopscotch-arrow')){ + this.arrowEl = node; + } + } + + // Set z-index and arrow placement + el.style.zIndex = step.zindex || ''; + this._setArrow(step.placement); + + // Set bubble positioning + // Make sure we're using visibility:hidden instead of display:none for height/width calculations. + this.hide(false); + this.setPosition(step); + + // only want to adjust window scroll for non-fixed elements + if (callback) { + callback(!step.fixedElement); + } + + return this; + }, + + /** + * Get the I18N step number for the current step. + * + * @private + */ + _getStepI18nNum: function(idx) { + var stepNumI18N = utils.getI18NString('stepNums'); + if (stepNumI18N && idx < stepNumI18N.length) { + idx = stepNumI18N[idx]; + } + else { + idx = idx + 1; + } + return idx; + }, + + /** + * Sets which side the arrow is on. + * + * @private + */ + _setArrow: function(orientation) { + utils.removeClass(this.arrowEl, 'down up right left'); + + // 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') { + utils.addClass(this.arrowEl, 'down'); + } + else if (orientation === 'bottom') { + utils.addClass(this.arrowEl, 'up'); + } + else if (orientation === 'left') { + utils.addClass(this.arrowEl, 'right'); + } + else if (orientation === 'right') { + utils.addClass(this.arrowEl, 'left'); + } + }, + + /** + * @private + */ + _getArrowDirection: function() { + if (this.placement === 'top') { + return 'down'; + } + if (this.placement === 'bottom') { + return 'up'; + } + if (this.placement === 'left') { + return 'right'; + } + if (this.placement === 'right') { + return 'left'; + } + }, + + show: function() { + var self = this, + fadeClass = 'fade-in-' + this._getArrowDirection(), + fadeDur = 1000; + + utils.removeClass(this.element, 'hide'); + utils.addClass(this.element, fadeClass); + setTimeout(function() { + utils.removeClass(self.element, 'invisible'); + }, 50); + setTimeout(function() { + utils.removeClass(self.element, fadeClass); + }, fadeDur); + this.isShowing = true; + return this; + }, + + hide: function(remove) { + var el = this.element; + + remove = utils.valOrDefault(remove, true); + el.style.top = ''; + el.style.left = ''; + + // display: none + if (remove) { + utils.addClass(el, 'hide'); + utils.removeClass(el, 'invisible'); + } + // opacity: 0 + else { + utils.removeClass(el, 'hide'); + utils.addClass(el, 'invisible'); + } + utils.removeClass(el, 'animate fade-in-up fade-in-down fade-in-right fade-in-left'); + this.isShowing = false; + return this; + }, + + destroy: function() { + var el = this.element; + + if (el) { + el.parentNode.removeChild(el); + } + utils.removeEvtListener(el, 'click', this.clickCb); + }, + + _handleBubbleClick: function(evt){ + var action; + + //Recursively look up the parent tree until we find a match + //with one of the classes we're looking for, or the triggering element. + function findMatchRecur(el){ + /* We're going to make the assumption that we're not binding + * multiple event classes to the same element. + * (next + previous = wait... err... what?) + * + * In the odd event we end up with an element with multiple + * possible matches, the following priority order is applied: + * hopscotch-cta, hopscotch-next, hopscotch-prev, hopscotch-close + */ + if(el === evt.currentTarget){ return null; } + if(utils.hasClass(el, 'hopscotch-cta')){ return 'cta'; } + if(utils.hasClass(el, 'hopscotch-next')){ return 'next'; } + if(utils.hasClass(el, 'hopscotch-prev')){ return 'prev'; } + if(utils.hasClass(el, 'hopscotch-close')){ return 'close'; } + /*else*/ return findMatchRecur(el.parentElement); + } + + action = findMatchRecur(evt.target); + + //Now that we know what action we should take, let's take it. + if (action === 'cta'){ + if (!this.opt.isTourBubble) { + // This is a callout. Close the callout when CTA is clicked. + winHopscotch.getCalloutManager().removeCallout(this.currStep.id); + } + // Call onCTA callback if one is provided + if (this.currStep.onCTA) { + utils.invokeCallback(this.currStep.onCTA); + } + } + else if (action === 'next'){ + winHopscotch.nextStep(true); + } + else if (action === 'prev'){ + winHopscotch.prevStep(true); + } + else if (action === 'close'){ + if (this.opt.isTourBubble){ + var currStepNum = winHopscotch.getCurrStepNum(), + currTour = winHopscotch.getCurrTour(), + doEndCallback = (currStepNum === currTour.steps.length-1); + + utils.invokeEventCallbacks('close'); + + winHopscotch.endTour(true, doEndCallback); + } else { + if (this.opt.onClose) { + utils.invokeCallback(this.opt.onClose); + } + if (this.opt.id && !this.opt.isTourBubble) { + // Remove via the HopscotchCalloutManager. + // removeCallout() calls HopscotchBubble.destroy internally. + winHopscotch.getCalloutManager().removeCallout(this.opt.id); + } + else { + this.destroy(); + } + } + + utils.evtPreventDefault(evt); + } + //Otherwise, do nothing. We didn't click on anything relevant. + }, + + init: function(initOpt) { + var el = document.createElement('div'), + self = this, + resizeCooldown = false, // for updating after window resize + onWinResize, + appendToBody, + children, + numChildren, + node, + i, + opt; + + //Register DOM element for this bubble. + this.element = el; + + //Merge bubble options with defaults. + opt = { + showPrevButton: defaultOpts.showPrevButton, + showNextButton: defaultOpts.showNextButton, + bubbleWidth: defaultOpts.bubbleWidth, + bubblePadding: defaultOpts.bubblePadding, + arrowWidth: defaultOpts.arrowWidth, + showNumber: true, + isTourBubble: true + }; + initOpt = (typeof initOpt === undefinedStr ? {} : initOpt); + utils.extend(opt, initOpt); + this.opt = opt; + + //Apply classes to bubble. Add "animated" for fade css animation + el.className = 'hopscotch-bubble animated'; + if (!opt.isTourBubble) { + utils.addClass(el, 'hopscotch-callout no-number'); + } + + /** + * Not pretty, but IE8 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 + * + * @private + */ + onWinResize = function() { + if (resizeCooldown || !self.isShowing) { + return; + } + + resizeCooldown = true; + setTimeout(function() { + self.setPosition(self.currStep); + resizeCooldown = false; + }, 100); + }; + + //Add listener to reset bubble position on window resize + utils.addEvtListener(window, 'resize', onWinResize); + + //Create our click callback handler and keep a + //reference to it for later. + this.clickCb = function(evt){ + self._handleBubbleClick(evt); + }; + utils.addEvtListener(el, 'click', this.clickCb); + + //Hide the bubble by default + this.hide(); + + //Finally, append our new bubble to body once the DOM is ready. + if (utils.documentIsReady()) { + document.body.appendChild(el); + } + else { + // Moz, webkit, Opera + if (document.addEventListener) { + appendToBody = function() { + document.removeEventListener('DOMContentLoaded', appendToBody); + window.removeEventListener('load', appendToBody); + + document.body.appendChild(el); + }; + + document.addEventListener('DOMContentLoaded', appendToBody, false); + } + // IE + else { + appendToBody = function() { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', appendToBody); + window.detachEvent('onload', appendToBody); + document.body.appendChild(el); + } + }; + + document.attachEvent('onreadystatechange', appendToBody); + } + utils.addEvtListener(window, 'load', appendToBody); + } + } + }; + + /** + * HopscotchCalloutManager + * + * @class Manages the creation and destruction of single callouts. + * @constructor + */ + HopscotchCalloutManager = function() { + var callouts = {}; + + /** + * createCallout + * + * Creates a standalone callout. This callout has the same API + * as a Hopscotch tour bubble. + * + * @param {Object} opt The options for the callout. For the most + * part, these are the same options as you would find in a tour + * step. + */ + this.createCallout = function(opt) { + var callout; + + if (opt.id) { + if (callouts[opt.id]) { + throw 'Callout by that id already exists. Please choose a unique id.'; + } + opt.showNextButton = opt.showPrevButton = false; + opt.isTourBubble = false; + callout = new HopscotchBubble(opt); + callouts[opt.id] = callout; + if (opt.target) { + callout.render(opt, null, function() { + callout.show(); + }); + } + } + else { + throw 'Must specify a callout id.'; + } + return callout; + }; + + /** + * getCallout + * + * Returns a callout by its id. + * + * @param {String} id The id of the callout to fetch. + * @returns {Object} HopscotchBubble + */ + this.getCallout = function(id) { + return callouts[id]; + }; + + /** + * removeAllCallouts + * + * Removes all existing callouts. + */ + this.removeAllCallouts = function() { + var calloutId, + callout; + + for (calloutId in callouts) { + if (callouts.hasOwnProperty(calloutId)) { + this.removeCallout(calloutId); + } + } + }; + + /** + * removeAllCallout + * + * Removes an existing callout by id. + * + * @param {String} id The id of the callout to remove. + */ + this.removeCallout = function(id) { + var callout = callouts[id]; + + callouts[id] = null; + if (!callout) { return; } + + callout.destroy(); + }; + }; + + /** + * Hopscotch + * + * @class Creates the Hopscotch object. Used to manage tour progress and configurations. + * @constructor + * @param {Object} initOptions Options to be passed to `configure()`. + */ + Hopscotch = function(initOptions) { + var self = this, // for targetClickNextFn + bubble, + calloutMgr, + opt, + currTour, + currStepNum, + cookieTourId, + cookieTourStep, + _configure, + + /** + * getBubble + * + * Singleton accessor function for retrieving or creating bubble object. + * + * @private + * @param setOptions {Boolean} when true, transfers configuration options to the bubble + * @returns {Object} HopscotchBubble + */ + getBubble = function(setOptions) { + if (!bubble) { + bubble = new HopscotchBubble(opt); + } + if (setOptions) { + utils.extend(bubble.opt, { + bubblePadding: getOption('bubblePadding'), + bubbleWidth: getOption('bubbleWidth'), + showNextButton: getOption('showNextButton'), + showPrevButton: getOption('showPrevButton'), + showCloseButton: getOption('showCloseButton'), + arrowWidth: getOption('arrowWidth') + }); + } + return bubble; + }, + + /** + * Convenience method for getting an option. Returns custom config option + * or the default config option if no custom value exists. + * + * @private + * @param name {String} config option name + * @returns {Object} config option value + */ + getOption = function(name) { + if (typeof opt === 'undefined') { + return defaultOpts[name]; + } + return utils.valOrDefault(opt[name], defaultOpts[name]); + }, + + /** + * getCurrStep + * + * @private + * @returns {Object} the step object corresponding to the current value of currStepNum + */ + getCurrStep = function() { + var step; + + if (currStepNum < 0 || currStepNum >= currTour.steps.length) { + step = null; + } + else { + step = currTour.steps[currStepNum]; + } + + return step; + }, + + /** + * Used for nextOnTargetClick + * + * @private + */ + targetClickNextFn = function() { + self.nextStep(); + }, + + /** + * adjustWindowScroll + * + * Checks if the bubble or target element is partially or completely + * outside of the viewport. If it is, adjust the window scroll position + * to bring it back into the viewport. + * + * @private + * @param {Function} cb Callback to invoke after done scrolling. + */ + adjustWindowScroll = function(cb) { + var bubble = getBubble(), + + // Calculate the bubble element top and bottom position + bubbleEl = bubble.element, + bubbleTop = utils.getPixelValue(bubbleEl.style.top), + bubbleBottom = bubbleTop + utils.getPixelValue(bubbleEl.offsetHeight), + + // Calculate the target element top and bottom position + targetEl = utils.getStepTarget(getCurrStep()), + targetBounds = targetEl.getBoundingClientRect(), + targetElTop = targetBounds.top + utils.getScrollTop(), + targetElBottom = targetBounds.bottom + utils.getScrollTop(), + + // The higher of the two: bubble or target + targetTop = (bubbleTop < targetElTop) ? bubbleTop : targetElTop, + // The lower of the two: bubble or target + targetBottom = (bubbleBottom > targetElBottom) ? bubbleBottom : targetElBottom, + + // Calculate the current viewport top and bottom + windowTop = utils.getScrollTop(), + windowBottom = windowTop + utils.getWindowHeight(), + + // This is our final target scroll value. + scrollToVal = targetTop - getOption('scrollTopMargin'), + + scrollEl, + yuiAnim, + yuiEase, + direction, + scrollIncr, + scrollTimeout, + scrollTimeoutFn; + + // Target and bubble are both visible in viewport + if (targetTop >= windowTop && (targetTop <= windowTop + getOption('scrollTopMargin') || targetBottom <= windowBottom)) { + if (cb) { cb(); } // HopscotchBubble.show + } + + // Abrupt scroll to scroll target + else if (!getOption('smoothScroll')) { + window.scrollTo(0, scrollToVal); + + if (cb) { cb(); } // HopscotchBubble.show + } + + // Smooth scroll to scroll target + else { + // Use YUI if it exists + if (typeof YAHOO !== undefinedStr && + typeof YAHOO.env !== undefinedStr && + typeof YAHOO.env.ua !== undefinedStr && + typeof YAHOO.util !== undefinedStr && + typeof YAHOO.util.Scroll !== undefinedStr) { + scrollEl = YAHOO.env.ua.webkit ? document.body : document.documentElement; + yuiEase = YAHOO.util.Easing ? YAHOO.util.Easing.easeOut : undefined; + yuiAnim = new YAHOO.util.Scroll(scrollEl, { + scroll: { to: [0, scrollToVal] } + }, getOption('scrollDuration')/1000, yuiEase); + yuiAnim.onComplete.subscribe(cb); + yuiAnim.animate(); + } + + // Use jQuery if it exists + else if (hasJquery) { + jQuery('body, html').animate({ scrollTop: scrollToVal }, getOption('scrollDuration'), cb); + } + + // Use my crummy setInterval scroll solution if we're using plain, vanilla Javascript. + else { + if (scrollToVal < 0) { + scrollToVal = 0; + } + + // 48 * 10 == 480ms scroll duration + // make it slightly less than CSS transition duration because of + // setInterval overhead. + // To increase or decrease duration, change the divisor of scrollIncr. + direction = (windowTop > targetTop) ? -1 : 1; // -1 means scrolling up, 1 means down + scrollIncr = Math.abs(windowTop - scrollToVal) / (getOption('scrollDuration')/10); + scrollTimeoutFn = function() { + var scrollTop = utils.getScrollTop(), + scrollTarget = scrollTop + (direction * scrollIncr); + + if ((direction > 0 && scrollTarget >= scrollToVal) || + (direction < 0 && scrollTarget <= scrollToVal)) { + // Overshot our target. Just manually set to equal the target + // and clear the interval + scrollTarget = scrollToVal; + if (cb) { cb(); } // HopscotchBubble.show + window.scrollTo(0, scrollTarget); + return; + } + + window.scrollTo(0, scrollTarget); + + if (utils.getScrollTop() === scrollTop) { + // Couldn't scroll any further. + if (cb) { cb(); } // HopscotchBubble.show + return; + } + + // If we reached this point, that means there's still more to scroll. + setTimeout(scrollTimeoutFn, 10); + }; + + scrollTimeoutFn(); + } + } + }, + + /** + * goToStepWithTarget + * + * Helper function to increment the step number until a step is found where + * the step target exists or until we reach the end/beginning of the tour. + * + * @private + * @param {Number} direction Either 1 for incrementing or -1 for decrementing + * @param {Function} cb The callback function to be invoked when the step has been found + */ + goToStepWithTarget = function(direction, cb) { + var target, + step, + goToStepFn; + + if (currStepNum + direction >= 0 && + currStepNum + direction < currTour.steps.length) { + + currStepNum += direction; + step = getCurrStep(); + + goToStepFn = function() { + target = utils.getStepTarget(step); + + if (target) { + // We're done! Return the step number via the callback. + cb(currStepNum); + } + else { + // Haven't found a valid target yet. Recursively call + // goToStepWithTarget. + utils.invokeEventCallbacks('error'); + goToStepWithTarget(direction, cb); + } + }; + + if (step.delay) { + setTimeout(goToStepFn, step.delay); + } + else { + goToStepFn(); + } + } + else { + cb(-1); // signal that we didn't find any step with a valid target + } + }, + + /** + * changeStep + * + * Helper function to change step by going forwards or backwards 1. + * nextStep and prevStep are publicly accessible wrappers for this function. + * + * @private + * @param {Boolean} doCallbacks Flag for invoking onNext or onPrev callbacks + * @param {Number} direction Either 1 for "next" or -1 for "prev" + */ + changeStep = function(doCallbacks, direction) { + var bubble = getBubble(), + self = this, + step, + origStep, + wasMultiPage, + changeStepCb; + + bubble.hide(); + + doCallbacks = utils.valOrDefault(doCallbacks, true); + step = getCurrStep(); + origStep = step; + if (direction > 0) { + wasMultiPage = origStep.multipage; + } + else { + wasMultiPage = (currStepNum > 0 && currTour.steps[currStepNum-1].multipage); + } + + /** + * Callback for goToStepWithTarget + * + * @private + */ + changeStepCb = function(stepNum) { + var doShowFollowingStep; + + if (stepNum === -1) { + // Wasn't able to find a step with an existing element. End tour. + return this.endTour(true); + } + + if (doCallbacks) { + if (direction > 0) { + doShowFollowingStep = utils.invokeEventCallbacks('next', origStep.onNext); + } + else { + doShowFollowingStep = utils.invokeEventCallbacks('prev', origStep.onPrev); + } + } + + // If the state of the tour is updated in a callback, assume the client + // doesn't want to go to next step since they specifically updated. + if (stepNum !== currStepNum) { + return; + } + + if (wasMultiPage) { + // Update state for the next page + utils.setState(getOption('cookieName'), currTour.id + ':' + currStepNum, 1); + + // Next step is on a different page, so no need to attempt to render it. + return; + } + + doShowFollowingStep = utils.valOrDefault(doShowFollowingStep, true); + + // If the onNext/onPrev callback returned false, halt the tour and + // don't show the next step. + if (doShowFollowingStep) { + this.showStep(stepNum); + } + else { + // Halt tour (but don't clear state) + this.endTour(false); + } + }; + + if (!wasMultiPage && getOption('skipIfNoElement')) { + goToStepWithTarget(direction, function(stepNum) { + changeStepCb.call(self, stepNum); + }); + } + else if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) { + // only try incrementing once, and invoke error callback if no target is found + currStepNum += direction; + step = getCurrStep(); + if (!utils.getStepTarget(step) && !wasMultiPage) { + utils.invokeEventCallbacks('error'); + return this.endTour(true, false); + } + changeStepCb.call(this, currStepNum); + } + + return this; + }, + + /** + * loadTour + * + * Loads, but does not display, tour. + * + * @private + * @param tour The tour JSON object + */ + loadTour = function(tour) { + var tmpOpt = {}, + prop, + tourState, + tourPair; + + // Set tour-specific configurations + for (prop in tour) { + if (tour.hasOwnProperty(prop) && + prop !== 'id' && + prop !== 'steps') { + tmpOpt[prop] = tour[prop]; + } + } + + //this.resetDefaultOptions(); // reset all options so there are no surprises + // TODO check number of config properties of tour + _configure.call(this, tmpOpt, true); + + // Get existing tour state, if it exists. + tourState = utils.getState(getOption('cookieName')); + if (tourState) { + tourPair = tourState.split(':'); + cookieTourId = tourPair[0]; // selecting tour is not supported by this framework. + cookieTourStep = tourPair[1]; + + cookieTourStep = parseInt(cookieTourStep, 10); + } + + return this; + }, + + /** + * Find the first step to show for a tour. (What is the first step with a + * target on the page?) + */ + findStartingStep = function(startStepNum, cb) { + var step, + target, + stepNum; + + currStepNum = startStepNum || 0; + step = getCurrStep(); + target = utils.getStepTarget(step); + + if (target) { + // First step had an existing target. + cb(currStepNum); + return; + } + + if (!target) { + // Previous target doesn't exist either. The user may have just + // clicked on a link that wasn't part of the tour. Another possibility is that + // the user clicked on the correct link, but the target is just missing for + // whatever reason. In either case, we should just advance until we find a step + // that has a target on the page or end the tour if we can't find such a step. + utils.invokeEventCallbacks('error'); + + if (getOption('skipIfNoElement')) { + goToStepWithTarget(1, cb); + return; + } + else { + currStepNum = -1; + cb(currStepNum); + } + } + }, + + showStepHelper = function(stepNum) { + var step = currTour.steps[stepNum], + tourSteps = currTour.steps, + numTourSteps = tourSteps.length, + cookieVal = currTour.id + ':' + stepNum, + bubble = getBubble(), + targetEl = utils.getStepTarget(step), + isLast, + showBubble; + + showBubble = function() { + bubble.show(); + utils.invokeEventCallbacks('show', step.onShow); + }; + + // Update bubble for current step + currStepNum = stepNum; + + bubble.hide(false); + + isLast = (stepNum === numTourSteps - 1); + bubble.render(step, stepNum, function(adjustScroll) { + // when done adjusting window scroll, call showBubble helper fn + if (adjustScroll) { + adjustWindowScroll(showBubble); + } + else { + showBubble(); + } + + // If we want to advance to next step when user clicks on target. + if (step.nextOnTargetClick) { + utils.addEvtListener(targetEl, 'click', targetClickNextFn); + } + }); + + utils.setState(getOption('cookieName'), cookieVal, 1); + }, + + /** + * init + * + * Initializes the Hopscotch object. + * + * @private + */ + init = function(initOptions) { + if (initOptions) { + //initOptions.cookieName = initOptions.cookieName || 'hopscotch.tour.state'; + this.configure(initOptions); + } + }; + + /** + * getCalloutManager + * + * Gets the callout manager. + * + * @returns {Object} HopscotchCalloutManager + * + */ + this.getCalloutManager = function() { + if (typeof calloutMgr === undefinedStr) { + calloutMgr = new HopscotchCalloutManager(); + } + + return calloutMgr; + }; + + /** + * startTour + * + * Begins the tour. + * + * @param {Object} tour The tour JSON object + * @stepNum {Number} stepNum __Optional__ The step number to start from + * @returns {Object} Hopscotch + * + */ + this.startTour = function(tour, stepNum) { + var bubble, + currStepNum, + self = this; + + // loadTour if we are calling startTour directly. (When we call startTour + // from window onLoad handler, we'll use currTour) + if (!currTour) { + currTour = tour; + loadTour.call(this, tour); + } + + if (typeof stepNum !== undefinedStr) { + if (stepNum >= currTour.steps.length) { + throw 'Specified step number out of bounds.'; + } + currStepNum = stepNum; + } + + // If document isn't ready, wait for it to finish loading. + // (so that we can calculate positioning accurately) + if (!utils.documentIsReady()) { + waitingToStart = true; + return this; + } + + if (typeof currStepNum === "undefined" && currTour.id === cookieTourId && typeof cookieTourStep !== undefinedStr) { + currStepNum = cookieTourStep; + } + else if (!currStepNum) { + currStepNum = 0; + } + + // Find the current step we should begin the tour on, and then actually start the tour. + findStartingStep(currStepNum, function(stepNum) { + var target = (stepNum !== -1) && utils.getStepTarget(currTour.steps[stepNum]); + + if (!target) { + // Should we trigger onEnd callback? Let's err on the side of caution + // and not trigger it. Don't want weird stuff happening on a page that + // wasn't meant for the tour. Up to the developer to fix their tour. + self.endTour(false, false); + return; + } + + utils.invokeEventCallbacks('start'); + + bubble = getBubble(); + // TODO: do we still need this call to .hide()? No longer using opt.animate... + // Leaving it in for now to play it safe + bubble.hide(false); // make invisible for boundingRect calculations when opt.animate == true + + self.isActive = true; + + if (!utils.getStepTarget(getCurrStep())) { + // First step element doesn't exist + utils.invokeEventCallbacks('error'); + if (getOption('skipIfNoElement')) { + self.nextStep(false); + } + } + else { + self.showStep(stepNum); + } + }); + + return this; + }; + + /** + * showStep + * + * Skips to a specific step and renders the corresponding bubble. + * + * @stepNum {Number} stepNum The step number to show + * @returns {Object} Hopscotch + */ + this.showStep = function(stepNum) { + var step = currTour.steps[stepNum]; + if (step.delay) { + setTimeout(function() { + showStepHelper(stepNum); + }, step.delay); + } + else { + showStepHelper(stepNum); + } + return this; + }; + + /** + * prevStep + * + * Jump to the previous step. + * + * @param {Boolean} doCallbacks Flag for invoking onPrev callback. Defaults to true. + * @returns {Object} Hopscotch + */ + this.prevStep = function(doCallbacks) { + changeStep.call(this, doCallbacks, -1); + return this; + }; + + /** + * nextStep + * + * Jump to the next step. + * + * @param {Boolean} doCallbacks Flag for invoking onNext callback. Defaults to true. + * @returns {Object} Hopscotch + */ + this.nextStep = function(doCallbacks) { + var step = getCurrStep(), + targetEl = utils.getStepTarget(step); + + if (step.nextOnTargetClick) { + // Detach the listener after we've clicked on the target OR the next button. + utils.removeEvtListener(targetEl, 'click', targetClickNextFn); + } + changeStep.call(this, doCallbacks, 1); + return this; + }; + + /** + * endTour + * + * Cancels out of an active tour. + * + * @param {Boolean} clearState Flag for clearing state. Defaults to true. + * @param {Boolean} doCallbacks Flag for invoking 'onEnd' callbacks. Defaults to true. + * @returns {Object} Hopscotch + */ + this.endTour = function(clearState, doCallbacks) { + var bubble = getBubble(); + clearState = utils.valOrDefault(clearState, true); + doCallbacks = utils.valOrDefault(doCallbacks, true); + currStepNum = 0; + cookieTourStep = undefined; + + bubble.hide(); + if (clearState) { + utils.clearState(getOption('cookieName')); + } + if (this.isActive) { + this.isActive = false; + + if (currTour && doCallbacks) { + utils.invokeEventCallbacks('end'); + } + } + + this.removeCallbacks(null, true); + this.resetDefaultOptions(); + + currTour = null; + + return this; + }; + + /** + * getCurrTour + * + * @return {Object} The currently loaded tour. + */ + this.getCurrTour = function() { + return currTour; + }; + + /** + * getCurrTarget + * + * @return {Object} The currently visible target. + */ + this.getCurrTarget = function() { + return utils.getStepTarget(getCurrStep()); + }; + + /** + * getCurrStepNum + * + * @return {number} The current zero-based step number. + */ + this.getCurrStepNum = function() { + return currStepNum; + }; + + /** + * refreshBubblePosition + * + * Tell hopscotch that the position of the current tour element changed + * and the bubble therefore needs to be redrawn + * + * @returns {Object} Hopscotch + */ + this.refreshBubblePosition = function() { + bubble.setPosition(getCurrStep()); + return this; + }; + + /** + * listen + * + * Adds a callback for one of the event types. Valid event types are: + * + * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error" + * @param {Function} cb The callback to add. + * @param {Boolean} isTourCb Flag indicating callback is from a tour definition. + * For internal use only! + * @returns {Object} Hopscotch + */ + this.listen = function(evtType, cb, isTourCb) { + if (evtType) { + callbacks[evtType].push({ cb: cb, fromTour: isTourCb }); + } + return this; + }; + + /** + * unlisten + * + * Removes a callback for one of the event types, e.g. 'start', 'next', etc. + * + * @param {string} evtType "start", "end", "next", "prev", "show", "close", or "error" + * @param {Function} cb The callback to remove. + * @returns {Object} Hopscotch + */ + this.unlisten = function(evtType, cb) { + var evtCallbacks = callbacks[evtType], + i, + len; + + for (i = 0, len = evtCallbacks.length; i < len; ++i) { + if (evtCallbacks[i] === cb) { + evtCallbacks.splice(i, 1); + } + } + return this; + }; + + /** + * removeCallbacks + * + * Remove callbacks for hopscotch events. If tourOnly is set to true, only + * removes callbacks specified by a tour (callbacks set by external calls + * to hopscotch.configure or hopscotch.listen will not be removed). If + * evtName is null or undefined, callbacks for all events will be removed. + * + * @param {string} evtName Optional Event name for which we should remove callbacks + * @param {boolean} tourOnly Optional flag to indicate we should only remove callbacks added + * by a tour. Defaults to false. + * @returns {Object} Hopscotch + */ + this.removeCallbacks = function(evtName, tourOnly) { + var cbArr, + i, + len, + evt; + + // If evtName is null or undefined, remove callbacks for all events. + for (evt in callbacks) { + if (!evtName || evtName === evt) { + if (tourOnly) { + cbArr = callbacks[evt]; + for (i=0, len=cbArr.length; i < len; ++i) { + if (cbArr[i].fromTour) { + cbArr.splice(i--, 1); + --len; + } + } + } + else { + callbacks[evt] = []; + } + } + } + return this; + }; + + /** + * registerHelper + * ============== + * Registers a helper function to be used as a callback function. + * + * @param {String} id The id of the function. + * @param {Function} id The callback function. + */ + this.registerHelper = function(id, fn) { + if (typeof id === 'string' && typeof fn === 'function') { + helpers[id] = fn; + } + }; + + this.unregisterHelper = function(id) { + helpers[id] = null; + }; + + this.invokeHelper = function(id) { + var args = [], + i, + len; + + for (i = 1, len = arguments.length; i < len; ++i) { + args.push(arguments[i]); + } + if (helpers[id]) { + helpers[id].call(null, args); + } + }; + + /** + * setCookieName + * + * Sets the cookie name (or sessionStorage name, if supported) used for multi-page + * tour persistence. + * + * @param {String} name The cookie name + * @returns {Object} Hopscotch + */ + this.setCookieName = function(name) { + opt.cookieName = name; + return this; + }; + + /** + * resetDefaultOptions + * + * Resets all configuration options to default. + * + * @returns {Object} Hopscotch + */ + this.resetDefaultOptions = function() { + opt = {}; + return this; + }; + + /** + * resetDefaultI18N + * + * Resets all i18n. + * + * @returns {Object} Hopscotch + */ + this.resetDefaultI18N = function() { + customI18N = {}; + return this; + }; + + /** + * hasState + * + * Returns state from a previous tour run, if it exists. + * + * @returns {String} State of previous tour run, or empty string if none exists. + */ + this.getState = function() { + return utils.getState(getOption('cookieName')); + }; + + /** + * _configure + * + * @see this.configure + * @private + * @param options + * @param {Boolean} isTourOptions Should be set to true when setting options from a tour definition. + */ + _configure = function(options, isTourOptions) { + var bubble, + events = ['next', 'prev', 'start', 'end', 'show', 'error', 'close'], + eventPropName, + callbackProp, + i, + len; + + if (!opt) { + this.resetDefaultOptions(); + } + + utils.extend(opt, options); + + if (options) { + utils.extend(customI18N, options.i18n); + } + + for (i = 0, len = events.length; i < len; ++i) { + // At this point, options[eventPropName] may have changed from an array + // to a function. + eventPropName = 'on' + events[i].charAt(0).toUpperCase() + events[i].substring(1); + if (options[eventPropName]) { + this.listen(events[i], + options[eventPropName], + isTourOptions); + } + } + + bubble = getBubble(true); + + return this; + }; + + /** + * configure + * + *
+     * VALID OPTIONS INCLUDE...
+     *
+     * - bubbleWidth:     Number   - Default bubble width. Defaults to 280.
+     * - bubblePadding:   Number   - DEPRECATED. Default bubble padding. Defaults to 15.
+     * - smoothScroll:    Boolean  - should the page scroll smoothly to the next
+     *                               step? Defaults to TRUE.
+     * - scrollDuration:  Number   - Duration of page scroll. Only relevant when
+     *                               smoothScroll is set to true. Defaults to
+     *                               1000ms.
+     * - scrollTopMargin: NUMBER   - When the page scrolls, how much space should there
+     *                               be between the bubble/targetElement and the top
+     *                               of the viewport? Defaults to 200.
+     * - showCloseButton: Boolean  - should the tour bubble show a close (X) button?
+     *                               Defaults to TRUE.
+     * - showPrevButton:  Boolean  - should the bubble have the Previous button?
+     *                               Defaults to FALSE.
+     * - showNextButton:  Boolean  - should the bubble have the Next button?
+     *                               Defaults to TRUE.
+     * - arrowWidth:      Number   - Default arrow width. (space between the bubble
+     *                               and the targetEl) Used for bubble position
+     *                               calculation. Only use this option if you are
+     *                               using your own custom CSS. Defaults to 20.
+     * - skipIfNoElement  Boolean  - If a specified target element is not found,
+     *                               should we skip to the next step? Defaults to
+     *                               TRUE.
+     * - onNext:          Function - A callback to be invoked after every click on
+     *                               a "Next" button.
+     *
+     * - i18n:            Object   - For i18n purposes. Allows you to change the
+     *                               text of button labels and step numbers.
+     * - i18n.stepNums:   Array\ - 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.
+     * // =========
+     * // CALLBACKS
+     * // =========
+     * - onNext:          Function - Invoked after every click on a "Next" button.
+     * - onPrev:          Function - Invoked after every click on a "Prev" button.
+     * - onStart:         Function - Invoked when the tour is started.
+     * - onEnd:           Function - Invoked when the tour ends.
+     * - onClose:         Function - Invoked when the user closes the tour before finishing.
+     * - onError:         Function - Invoked when the specified target element doesn't exist on the page.
+     *
+     * // ====
+     * // I18N
+     * // ====
+     * i18n:              OBJECT      - For i18n purposes. Allows you to change the text
+     *                                  of button labels and step numbers.
+     * i18n.nextBtn:      STRING      - Label for next button
+     * i18n.prevBtn:      STRING      - Label for prev button
+     * i18n.doneBtn:      STRING      - Label for done button
+     * i18n.skipBtn:      STRING      - Label for skip button
+     * i18n.closeTooltip: STRING      - Text for close button tooltip
+     * i18n.stepNums:   ARRAY - 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.
+     * 
+ * + * @example hopscotch.configure({ scrollDuration: 1000, scrollTopMargin: 150 }); + * @example + * hopscotch.configure({ + * scrollTopMargin: 150, + * onStart: function() { + * alert("Have fun!"); + * }, + * i18n: { + * nextBtn: 'Forward', + * prevBtn: 'Previous' + * closeTooltip: 'Quit' + * } + * }); + * + * @param {Object} options A hash of configuration options. + * @returns {Object} Hopscotch + */ + this.configure = function(options) { + return _configure.call(this, options, false); + }; + + /** + * Set the template that should be used for rendering Hopscotch bubbles. + * If a string, it's assumed your template is available in the + * hopscotch.templates namespace. + * + * @param {String|Function(obj)} The template to use for rendering. + * @returns {Object} The Hopscotch object (for chaining). + */ + this.setRenderer = function(render){ + var typeOfRender = typeof render; + + if(typeOfRender === 'string'){ + templateToUse = render; + customRenderer = undefined; + } + else if(typeOfRender === 'function'){ + customRenderer = render; + } + return this; + }; + + /** + * Sets the escaping method to be used by JST templates. + * + * @param {Function} - The escape method to use. + * @returns {Object} The Hopscotch object (for chaining). + */ + this.setEscaper = function(esc){ + if (typeof esc === 'function'){ + customEscape = esc; + } + return this; + }; + + init.call(this, initOptions); + }; + + winHopscotch = new Hopscotch(); + context[namespace] = winHopscotch; + +// Template includes, placed inside a closure to ensure we don't +// end up declaring our shim globally. +(function(){ +var _ = {}; +/* + * Adapted from the Underscore.js framework. Check it out at + * https://github.com/jashkenas/underscore + */ +_.escape = function(str){ + if(customEscape){ return customEscape(str); } + + if(str == null) return ''; + return ('' + str).replace(new RegExp('[&<>"\']', 'g'), function(match){ + if(match == '&'){ return '&' } + if(match == '<'){ return '<' } + if(match == '>'){ return '>' } + if(match == '"'){ return '"' } + if(match == "'"){ return ''' } + }); +} +this["hopscotch"] = this["hopscotch"] || {}; +this["hopscotch"]["templates"] = this["hopscotch"]["templates"] || {}; + +this["hopscotch"]["templates"]["bubble_default"] = function(obj) { +obj || (obj = {}); +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +with (obj) { + + + function optEscape(str, unsafe){ + if(unsafe){ + return _.escape(str); + } + return str; + } +; +__p += '\n
\n '; + if(tour.isTour){ ; +__p += '' + +((__t = ( i18n.stepNum )) == null ? '' : __t) + +''; + } ; +__p += '\n
\n '; + if(step.title !== ''){ ; +__p += '

' + +((__t = ( optEscape(step.title, tour.unsafe) )) == null ? '' : __t) + +'

'; + } ; +__p += '\n '; + if(step.content !== ''){ ; +__p += '
' + +((__t = ( optEscape(step.content, tour.unsafe) )) == null ? '' : __t) + +'
'; + } ; +__p += '\n
\n
\n '; + if(buttons.showPrev){ ; +__p += ''; + } ; +__p += '\n '; + if(buttons.showCTA){ ; +__p += ''; + } ; +__p += '\n '; + if(buttons.showNext){ ; +__p += ''; + } ; +__p += '\n
\n '; + if(buttons.showClose){ ; +__p += '' + +((__t = ( i18n.closeTooltip )) == null ? '' : __t) + +''; + } ; +__p += '\n
\n
\n
\n
\n
'; + +} +return __p +}; +}()); + diff --git a/ajax/libs/hopscotch/0.2.0/hopscotch.min.js b/ajax/libs/hopscotch/0.2.0/hopscotch.min.js new file mode 100644 index 000000000..e294047b8 --- /dev/null +++ b/ajax/libs/hopscotch/0.2.0/hopscotch.min.js @@ -0,0 +1,17 @@ +/**! hopscotch - v0.2.0 +* +* Copyright 2014 LinkedIn Corp. All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* 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(context,namespace){var Hopscotch,HopscotchBubble,HopscotchCalloutManager,HopscotchI18N,customI18N,customRenderer,customEscape,utils,callbacks,helpers,winLoadHandler,defaultOpts,templateToUse="bubble_default",Sizzle=window.Sizzle||null,winHopscotch=context[namespace],undefinedStr="undefined",waitingToStart=!1,hasJquery=typeof window.jQuery!==undefinedStr,hasSessionStorage=!1,document=window.document;try{hasSessionStorage=typeof window.sessionStorage!==undefinedStr}catch(err){}defaultOpts={smoothScroll:!0,scrollDuration:1e3,scrollTopMargin:200,showCloseButton:!0,showPrevButton:!1,showNextButton:!0,bubbleWidth:280,bubblePadding:15,arrowWidth:20,skipIfNoElement:!0,cookieName:"hopscotch.tour.state"},winHopscotch||(Array.isArray||(Array.isArray=function(a){return"[object Array]"===Object.prototype.toString.call(a)}),winLoadHandler=function(){waitingToStart&&winHopscotch.startTour()},utils={addClass:function(a,b){var c,d,e,f;if(a.className){for(d=b.split(/\s+/),c=" "+a.className+" ",e=0,f=d.length;f>e;++e)c.indexOf(" "+d[e]+" ")<0&&(c+=d[e]+" ");a.className=c.replace(/^\s+|\s+$/g,"")}else a.className=b},removeClass:function(a,b){var c,d,e,f;for(d=b.split(/\s+/),c=" "+a.className+" ",e=0,f=d.length;f>e;++e)c=c.replace(" "+d[e]+" "," ");a.className=c.replace(/^\s+|\s+$/g,"")},hasClass:function(a,b){var c;return a.className?(c=" "+a.className+" ",-1!==c.indexOf(" "+b+" ")):!1},getPixelValue:function(a){var b=typeof a;return"number"===b?a:"string"===b?parseInt(a,10):0},valOrDefault:function(a,b){return typeof a!==undefinedStr?a:b},invokeCallbackArrayHelper:function(a){var b;return Array.isArray(a)&&(b=helpers[a[0]],"function"==typeof b)?b.apply(this,a.slice(1)):void 0},invokeCallbackArray:function(a){var b,c;if(Array.isArray(a)){if("string"==typeof a[0])return utils.invokeCallbackArrayHelper(a);for(b=0,c=a.length;c>b;++b)utils.invokeCallback(a[b])}},invokeCallback:function(a){return"function"==typeof a?a():"string"==typeof a&&helpers[a]?helpers[a]():utils.invokeCallbackArray(a)},invokeEventCallbacks:function(a,b){var c,d,e=callbacks[a];if(b)return this.invokeCallback(b);for(c=0,d=e.length;d>c;++c)this.invokeCallback(e[c].cb)},getScrollTop:function(){var a;return a=typeof window.pageYOffset!==undefinedStr?window.pageYOffset:document.documentElement.scrollTop},getScrollLeft:function(){var a;return a=typeof window.pageXOffset!==undefinedStr?window.pageXOffset:document.documentElement.scrollLeft},getWindowHeight:function(){return window.innerHeight||document.documentElement.clientHeight},getWindowWidth:function(){return window.innerWidth||document.documentElement.clientWidth},addEvtListener:function(a,b,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)},removeEvtListener:function(a,b,c){return a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c)},documentIsReady:function(){return"complete"===document.readyState||"interactive"===document.readyState},evtPreventDefault:function(a){a.preventDefault?a.preventDefault():event&&(event.returnValue=!1)},extend:function(a,b){var c;for(c in b)b.hasOwnProperty(c)&&(a[c]=b[c])},getStepTargetHelper:function(a){var b=document.getElementById(a);return b?b:document.querySelector?document.querySelector(a):hasJquery?(b=jQuery(a),b.length?b[0]:null):Sizzle?(b=new Sizzle(a),b.length?b[0]:null):/^#[a-zA-Z][\w-_:.]*$/.test(a)?document.getElementById(a.substring(1)):null},getStepTarget:function(a){var b;if(!a||!a.target)return null;if("string"==typeof a.target)return a.target=utils.getStepTargetHelper(a.target),a.target;if(Array.isArray(a.target)){var c,d;for(c=0,d=a.target.length;d>c;c++)if("string"==typeof a.target[c]&&(b=utils.getStepTargetHelper(a.target[c])))return a.target=b,b;return null}return a.target},getI18NString:function(a){return customI18N[a]||HopscotchI18N[a]},setState:function(a,b,c){var d,e="";hasSessionStorage?sessionStorage.setItem(a,b):(c&&(d=new Date,d.setTime(d.getTime()+24*c*60*60*1e3),e="; expires="+d.toGMTString()),document.cookie=a+"="+b+e+"; path=/")},getState:function(a){var b,c,d,e=a+"=",f=document.cookie.split(";");if(hasSessionStorage)d=sessionStorage.getItem(a);else for(b=0;b0,showNext:utils.valOrDefault(a.showNextButton,this.opt.showNextButton),showCTA:utils.valOrDefault(a.showCTAButton&&a.ctaLabel,!1),ctaLabel:a.ctaLabel,showClose:utils.valOrDefault(this.opt.showCloseButton,!0)},step:{num:b,isLast:utils.valOrDefault(k,!1),title:a.title||"",content:a.content||"",placement:a.placement,padding:utils.valOrDefault(a.padding,this.opt.bubblePadding),width:utils.getPixelValue(a.width)||this.opt.bubbleWidth,customData:a.customData||{}},tour:{isTour:this.opt.isTourBubble,numSteps:h,unsafe:utils.valOrDefault(f,!1),customData:e||{}}},"function"==typeof d)m.innerHTML=d(l);else if("string"==typeof d){if(!hopscotch.templates||"function"!=typeof hopscotch.templates[d])throw'Bubble rendering failed - template "'+d+'" is not a function.';m.innerHTML=hopscotch.templates[d](l)}else if(customRenderer)m.innerHTML=customRenderer(l);else{if(!hopscotch.templates||"function"!=typeof hopscotch.templates[templateToUse])throw'Bubble rendering failed - template "'+templateToUse+'" is not a function.';m.innerHTML=hopscotch.templates[templateToUse](l)}for(children=m.children,numChildren=children.length,i=0;numChildren>i;i++)node=children[i],utils.hasClass(node,"hopscotch-arrow")&&(this.arrowEl=node);return m.style.zIndex=a.zindex||"",this._setArrow(a.placement),this.hide(!1),this.setPosition(a),c&&c(!a.fixedElement),this},_getStepI18nNum:function(a){var b=utils.getI18NString("stepNums");return b&&af||f>=e.steps.length?null:e.steps[f]},n=function(){j.nextStep()},o=function(a){var b,c,d,e,f,g,h=k(),i=h.element,j=utils.getPixelValue(i.style.top),n=j+utils.getPixelValue(i.offsetHeight),o=utils.getStepTarget(m()),p=o.getBoundingClientRect(),q=p.top+utils.getScrollTop(),r=p.bottom+utils.getScrollTop(),s=q>j?j:q,t=n>r?n:r,u=utils.getScrollTop(),v=u+utils.getWindowHeight(),w=s-l("scrollTopMargin");s>=u&&(s<=u+l("scrollTopMargin")||v>=t)?a&&a():l("smoothScroll")?typeof YAHOO!==undefinedStr&&typeof YAHOO.env!==undefinedStr&&typeof YAHOO.env.ua!==undefinedStr&&typeof YAHOO.util!==undefinedStr&&typeof YAHOO.util.Scroll!==undefinedStr?(b=YAHOO.env.ua.webkit?document.body:document.documentElement,d=YAHOO.util.Easing?YAHOO.util.Easing.easeOut:void 0,c=new YAHOO.util.Scroll(b,{scroll:{to:[0,w]}},l("scrollDuration")/1e3,d),c.onComplete.subscribe(a),c.animate()):hasJquery?jQuery("body, html").animate({scrollTop:w},l("scrollDuration"),a):(0>w&&(w=0),e=u>s?-1:1,f=Math.abs(u-w)/(l("scrollDuration")/10),(g=function(){var b=utils.getScrollTop(),c=b+e*f;return e>0&&c>=w||0>e&&w>=c?(c=w,a&&a(),void window.scrollTo(0,c)):(window.scrollTo(0,c),utils.getScrollTop()===b?void(a&&a()):void setTimeout(g,10))})()):(window.scrollTo(0,w),a&&a())},p=function(a,b){var c,d,g;f+a>=0&&f+a0?d.multipage:f>0&&e.steps[f-1].multipage,h=function(c){var h;if(-1===c)return this.endTour(!0);if(a&&(h=b>0?utils.invokeEventCallbacks("next",d.onNext):utils.invokeEventCallbacks("prev",d.onPrev)),c===f){if(g)return void utils.setState(l("cookieName"),e.id+":"+f,1);h=utils.valOrDefault(h,!0),h?this.showStep(c):this.endTour(!1)}},!g&&l("skipIfNoElement"))p(b,function(a){h.call(j,a)});else if(f+b>=0&&f+b=e.steps.length)throw"Specified step number out of bounds.";d=b}return utils.documentIsReady()?("undefined"==typeof d&&e.id===g&&typeof h!==undefinedStr?d=h:d||(d=0),s(d,function(a){var b=-1!==a&&utils.getStepTarget(e.steps[a]);return b?(utils.invokeEventCallbacks("start"),c=k(),c.hide(!1),f.isActive=!0,void(utils.getStepTarget(m())?f.showStep(a):(utils.invokeEventCallbacks("error"),l("skipIfNoElement")&&f.nextStep(!1)))):void f.endTour(!1,!1)}),this):(waitingToStart=!0,this)},this.showStep=function(a){var b=e.steps[a];return b.delay?setTimeout(function(){t(a)},b.delay):t(a),this},this.prevStep=function(a){return q.call(this,a,-1),this},this.nextStep=function(a){var b=m(),c=utils.getStepTarget(b);return b.nextOnTargetClick&&utils.removeEvtListener(c,"click",n),q.call(this,a,1),this},this.endTour=function(a,b){var c=k();return a=utils.valOrDefault(a,!0),b=utils.valOrDefault(b,!0),f=0,h=void 0,c.hide(),a&&utils.clearState(l("cookieName")),this.isActive&&(this.isActive=!1,e&&b&&utils.invokeEventCallbacks("end")),this.removeCallbacks(null,!0),this.resetDefaultOptions(),e=null,this},this.getCurrTour=function(){return e},this.getCurrTarget=function(){return utils.getStepTarget(m())},this.getCurrStepNum=function(){return f},this.refreshBubblePosition=function(){return b.setPosition(m()),this},this.listen=function(a,b,c){return a&&callbacks[a].push({cb:b,fromTour:c}),this},this.unlisten=function(a,b){var c,d,e=callbacks[a];for(c=0,d=e.length;d>c;++c)e[c]===b&&e.splice(c,1);return this},this.removeCallbacks=function(a,b){var c,d,e,f;for(f in callbacks)if(!a||a===f)if(b)for(c=callbacks[f],d=0,e=c.length;e>d;++d)c[d].fromTour&&(c.splice(d--,1),--e);else callbacks[f]=[];return this},this.registerHelper=function(a,b){"string"==typeof a&&"function"==typeof b&&(helpers[a]=b)},this.unregisterHelper=function(a){helpers[a]=null},this.invokeHelper=function(a){var b,c,d=[];for(b=1,c=arguments.length;c>b;++b)d.push(arguments[b]);helpers[a]&&helpers[a].call(null,d)},this.setCookieName=function(a){return d.cookieName=a,this},this.resetDefaultOptions=function(){return d={},this},this.resetDefaultI18N=function(){return customI18N={},this},this.getState=function(){return utils.getState(l("cookieName"))},i=function(a,b){var c,e,f,g,h=["next","prev","start","end","show","error","close"];for(d||this.resetDefaultOptions(),utils.extend(d,a),a&&utils.extend(customI18N,a.i18n),f=0,g=h.length;g>f;++f)e="on"+h[f].charAt(0).toUpperCase()+h[f].substring(1),a[e]&&this.listen(h[f],a[e],b);return c=k(!0),this},this.configure=function(a){return i.call(this,a,!1)},this.setRenderer=function(a){var b=typeof a;return"string"===b?(templateToUse=a,customRenderer=void 0):"function"===b&&(customRenderer=a),this},this.setEscaper=function(a){return"function"==typeof a&&(customEscape=a),this},u.call(this,a)},winHopscotch=new Hopscotch,context[namespace]=winHopscotch,function(){var _={};_.escape=function(a){return customEscape?customEscape(a):null==a?"":(""+a).replace(new RegExp("[&<>\"']","g"),function(a){return"&"==a?"&":"<"==a?"<":">"==a?">":'"'==a?""":"'"==a?"'":void 0})},this.hopscotch=this.hopscotch||{},this.hopscotch.templates=this.hopscotch.templates||{},this.hopscotch.templates.bubble_default=function(obj){function optEscape(a,b){return b?_.escape(a):a}obj||(obj={});{var __t,__p="";_.escape,Array.prototype.join}with(obj)__p+='\n
\n ',tour.isTour&&(__p+=''+(null==(__t=i18n.stepNum)?"":__t)+""),__p+='\n
\n ',""!==step.title&&(__p+='

'+(null==(__t=optEscape(step.title,tour.unsafe))?"":__t)+"

"),__p+="\n ",""!==step.content&&(__p+='
'+(null==(__t=optEscape(step.content,tour.unsafe))?"":__t)+"
"),__p+='\n
\n
\n ',buttons.showPrev&&(__p+='"),__p+="\n ",buttons.showCTA&&(__p+='"),__p+="\n ",buttons.showNext&&(__p+='"),__p+="\n
\n ",buttons.showClose&&(__p+=''+(null==(__t=i18n.closeTooltip)?"":__t)+""),__p+='\n
\n
\n
\n
\n
';return __p}}())}(window,"hopscotch"); \ No newline at end of file diff --git a/ajax/libs/hopscotch/package.json b/ajax/libs/hopscotch/package.json new file mode 100644 index 000000000..9be27ea3d --- /dev/null +++ b/ajax/libs/hopscotch/package.json @@ -0,0 +1,41 @@ +{ + "name": "hopscotch", + "filename": "hopscotch.min.js", + "version": "0.2.0", + "description": "A framework to make it easy for developers to add product tours to their pages.", + "main": "Gruntfile.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "grunt test" + }, + "repository": { + "type": "git", + "url": "git://github.com/linkedin/hopscotch.git" + }, + "author": "Gordon Koo", + "license": "Apache 2.0", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/linkedin/hopscotch/issues" + }, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-jshint": "~0.6.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-copy": "~0.5.0", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-less": "~0.9.0", + "grunt-contrib-watch": "~0.5.1", + "grunt-contrib-compress": "~0.6.0", + "grunt-mocha": "~0.4.10", + "expect.js": "~0.2.0", + "jquery": "~2.1.0", + "grunt-bump": "~0.0.13", + "grunt-shell": "~0.3.1", + "grunt-contrib-jst": "~0.6.0", + "grunt-include-replace": "~1.2.0", + "sinon": "~1.9.0" + } +} \ No newline at end of file