From 73d334cd99dc992c3c1c98d7655e16ee667b4c80 Mon Sep 17 00:00:00 2001 From: Timothy Lindvall Date: Fri, 2 Jun 2017 13:29:20 -0700 Subject: [PATCH] Actually add new AMD artifacts to dist. --- dist/js/hopscotch_amd.js | 2507 ++++++++++++++++++++++++++++++++++ dist/js/hopscotch_amd.min.js | 17 + 2 files changed, 2524 insertions(+) create mode 100644 dist/js/hopscotch_amd.js create mode 100644 dist/js/hopscotch_amd.min.js diff --git a/dist/js/hopscotch_amd.js b/dist/js/hopscotch_amd.js new file mode 100644 index 00000000..e2bcea9b --- /dev/null +++ b/dist/js/hopscotch_amd.js @@ -0,0 +1,2507 @@ +/**! hopscotch - v0.3.0 +* +* Copyright 2017 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. +*/ +define('hopscotch', function () { 'use strict'; + + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + + /* global document */ + + var Hopscotch; + var HopscotchBubble; + var HopscotchCalloutManager; + var HopscotchI18N; + var customI18N; + var customRenderer; + var customEscape; + var templateToUse = 'bubble_default'; + var Sizzle = window.Sizzle || null; + var utils; + var callbacks; + var helpers; + var winLoadHandler; + var defaultOpts; + var winHopscotch; + var undefinedStr = 'undefined'; + var waitingToStart = false; + var hasJquery = (typeof jQuery === 'undefined' ? 'undefined' : _typeof(jQuery)) !== undefinedStr; + var hasSessionStorage = false; + var isStorageWritable = false; + var validIdRegEx = /^[a-zA-Z]+[a-zA-Z0-9_-]*$/; + var rtlMatches = { + left: 'right', + right: 'left' + }; + + // If cookies are disabled, accessing sessionStorage can throw an error. + // sessionStorage could also throw an error in Safari on write (even though it exists). + // So, we'll try writing to sessionStorage to verify it's available. + try { + if (_typeof(window.sessionStorage) !== undefinedStr) { + hasSessionStorage = true; + sessionStorage.setItem('hopscotch.test.storage', 'ok'); + sessionStorage.removeItem('hopscotch.test.storage'); + isStorageWritable = true; + } + } catch (err) {} + + defaultOpts = { + smoothScroll: true, + scrollDuration: 1000, + scrollTopMargin: 200, + showCloseButton: true, + showPrevButton: false, + showNextButton: true, + bubbleWidth: 280, + bubblePadding: 15, + arrowWidth: 20, + skipIfNoElement: true, + isRtl: false, + cookieName: 'hopscotch.tour.state' + }; + + 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 winLoadHandler() { + 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 addClass(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 removeClass(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 hasClass(domEl, classToCheck) { + var classes; + + if (!domEl.className) { + return false; + } + classes = ' ' + domEl.className + ' '; + return classes.indexOf(' ' + classToCheck + ' ') !== -1; + }, + + /** + * @private + */ + getPixelValue: function getPixelValue(val) { + var valType = typeof val === 'undefined' ? 'undefined' : _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 valOrDefault(val, valDefault) { + return (typeof val === 'undefined' ? 'undefined' : _typeof(val)) !== undefinedStr ? val : valDefault; + }, + + /** + * Invokes a single callback represented by an array. + * Example input: ["my_fn", "arg1", 2, "arg3"] + * @private + */ + invokeCallbackArrayHelper: function invokeCallbackArrayHelper(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 invokeCallbackArray(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 invokeCallback(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 invokeEventCallbacks(evtType, stepCb) { + var cbArr = callbacks[evtType], + callback, + fn, + i, + len; + + if (stepCb) { + return this.invokeCallback(stepCb); + } + + for (i = 0, len = cbArr.length; i < len; ++i) { + this.invokeCallback(cbArr[i].cb); + } + }, + + /** + * @private + */ + getScrollTop: function getScrollTop() { + var scrollTop; + if (_typeof(window.pageYOffset) !== undefinedStr) { + scrollTop = window.pageYOffset; + } else { + // Most likely IE <=8, which doesn't support pageYOffset + scrollTop = document.documentElement.scrollTop; + } + return scrollTop; + }, + + /** + * @private + */ + getScrollLeft: function getScrollLeft() { + var scrollLeft; + if (_typeof(window.pageXOffset) !== undefinedStr) { + scrollLeft = window.pageXOffset; + } else { + // Most likely IE <=8, which doesn't support pageXOffset + scrollLeft = document.documentElement.scrollLeft; + } + return scrollLeft; + }, + + /** + * @private + */ + getWindowHeight: function getWindowHeight() { + return window.innerHeight || document.documentElement.clientHeight; + }, + + /** + * @private + */ + addEvtListener: function addEvtListener(el, evtName, fn) { + if (el) { + return el.addEventListener ? el.addEventListener(evtName, fn, false) : el.attachEvent('on' + evtName, fn); + } + }, + + /** + * @private + */ + removeEvtListener: function removeEvtListener(el, evtName, fn) { + if (el) { + return el.removeEventListener ? el.removeEventListener(evtName, fn, false) : el.detachEvent('on' + evtName, fn); + } + }, + + documentIsReady: function documentIsReady() { + return document.readyState === 'complete'; + }, + + /** + * @private + */ + evtPreventDefault: function evtPreventDefault(evt) { + if (evt.preventDefault) { + evt.preventDefault(); + } else if (event) { + event.returnValue = false; + } + }, + + /** + * @private + */ + extend: function extend(obj1, obj2) { + var prop; + for (prop in obj2) { + if (obj2.hasOwnProperty(prop)) { + obj1[prop] = obj2[prop]; + } + } + }, + + /** + * Helper function to get a single target DOM element. We will try to + * locate the DOM element through several ways, in the following order: + * + * 1) Passing the string into document.querySelector + * 2) Passing the string to jQuery, if it exists + * 3) Passing the string to Sizzle, if it exists + * 4) Calling document.getElementById if it is a plain id + * + * Default case is to assume the string is a plain id and call + * document.getElementById on it. + * + * @private + */ + getStepTargetHelper: function getStepTargetHelper(target) { + var result = document.getElementById(target); + + //Backwards compatibility: assume the string is an id + if (result) { + return result; + } + if (hasJquery) { + result = jQuery(target); + return result.length ? result[0] : null; + } + if (Sizzle) { + result = new Sizzle(target); + return result.length ? result[0] : null; + } + if (document.querySelector) { + try { + return document.querySelector(target); + } catch (err) {} + } + // Regex test for id. Following the HTML 4 spec for valid id formats. + // (http://www.w3.org/TR/html4/types.html#type-id) + if (/^#[a-zA-Z][\w-_:.]*$/.test(target)) { + return document.getElementById(target.substring(1)); + } + + return null; + }, + + /** + * Given a step, returns the target DOM element associated with it. It is + * recommended to only assign one target per step. However, there are + * some use cases which require multiple step targets to be supplied. In + * this event, we will use the first target in the array that we can + * locate on the page. See the comments for getStepTargetHelper for more + * information. + * + * @private + */ + getStepTarget: function getStepTarget(step) { + var queriedTarget; + + if (!step || !step.target) { + return null; + } + + if (typeof step.target === 'string') { + //Just one target to test. Check and return its results. + return utils.getStepTargetHelper(step.target); + } else if (Array.isArray(step.target)) { + // Multiple items to check. Check each and return the first success. + // Assuming they are all strings. + var i, len; + + for (i = 0, len = step.target.length; i < len; i++) { + if (typeof step.target[i] === 'string') { + queriedTarget = utils.getStepTargetHelper(step.target[i]); + + if (queriedTarget) { + return queriedTarget; + } + } + } + return null; + } + + // Assume that the step.target is a DOM element + return step.target; + }, + + /** + * Convenience method for getting an i18n string. Returns custom i18n value + * or the default i18n value if no custom value exists. + * + * @private + */ + getI18NString: function getI18NString(key) { + return customI18N[key] || HopscotchI18N[key]; + }, + + // Tour session persistence for multi-page tours. Uses HTML5 sessionStorage if available, then + // falls back to using cookies. + // + // The following cookie-related logic is borrowed from: + // http://www.quirksmode.org/js/cookies.html + + /** + * @private + */ + setState: function setState(name, value, days) { + var expires = '', + date; + + if (hasSessionStorage && isStorageWritable) { + try { + sessionStorage.setItem(name, value); + } catch (err) { + isStorageWritable = false; + this.setState(name, value, days); + } + } else { + if (hasSessionStorage) { + //Clear out existing sessionStorage key so the new value we set to cookie gets read. + //(If we're here, we've run into an error while trying to write to sessionStorage). + sessionStorage.removeItem(name); + } + if (days) { + date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = '; expires=' + date.toGMTString(); + } + document.cookie = name + '=' + value + expires + '; path=/'; + } + }, + + /** + * @private + */ + getState: function getState(name) { + var nameEQ = name + '=', + ca = document.cookie.split(';'), + i, + c, + state; + + //return value from session storage if we have it + if (hasSessionStorage) { + state = sessionStorage.getItem(name); + if (state) { + return state; + } + } + + //else, try cookies + for (i = 0; i < ca.length; i++) { + c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + state = c.substring(nameEQ.length, c.length); + break; + } + } + + return state; + }, + + /** + * @private + */ + clearState: function clearState(name) { + if (hasSessionStorage) { + sessionStorage.removeItem(name); + } else { + this.setState(name, '', -1); + } + }, + + /** + * Originally called it orientation, but placement is more intuitive. + * Allowing both for now for backwards compatibility. + * @private + */ + normalizePlacement: function normalizePlacement(step) { + if (!step.placement && step.orientation) { + step.placement = step.orientation; + } + }, + + /** + * If step is right-to-left enabled, flip the placement and xOffset, but only once. + * @private + */ + flipPlacement: function flipPlacement(step) { + if (step.isRtl && !step._isFlipped) { + var props = ['orientation', 'placement'], + prop, + i; + if (step.xOffset) { + step.xOffset = -1 * this.getPixelValue(step.xOffset); + } + for (i in props) { + prop = props[i]; + if (step.hasOwnProperty(prop) && rtlMatches.hasOwnProperty(step[prop])) { + step[prop] = rtlMatches[step[prop]]; + } + } + step._isFlipped = true; + } + } + }; + + utils.addEvtListener(window, 'load', winLoadHandler); + + callbacks = { + next: [], + prev: [], + start: [], + end: [], + show: [], + error: [], + close: [] + }; + + /** + * helpers + * ======= + * A map of functions to be used as callback listeners. Functions are + * added to and removed from the map using the functions + * Hopscotch.registerHelper() and Hopscotch.unregisterHelper(). + */ + helpers = {}; + + HopscotchI18N = { + stepNums: null, + nextBtn: 'Next', + prevBtn: 'Back', + doneBtn: 'Done', + skipBtn: 'Skip', + closeTooltip: 'Close' + }; + + customI18N = {}; // Developer's custom i18n strings goes here. + + /** + * HopscotchBubble + * + * @class The HopscotchBubble class represents the view of a bubble. This class is also used for Hopscotch callouts. + */ + HopscotchBubble = function HopscotchBubble(opt) { + this.init(opt); + }; + + HopscotchBubble.prototype = { + isShowing: false, + + currStep: undefined, + + /** + * setPosition + * + * Sets the position of the bubble using the bounding rectangle of the + * target element and the orientation and offset information specified by + * the JSON. + */ + setPosition: function setPosition(step) { + var bubbleBoundingHeight, + bubbleBoundingWidth, + boundingRect, + top, + left, + arrowOffset, + verticalLeftPosition, + targetEl = utils.getStepTarget(step), + el = this.element, + arrowEl = this.arrowEl, + arrowPos = step.isRtl ? 'right' : 'left'; + + utils.flipPlacement(step); + utils.normalizePlacement(step); + + bubbleBoundingWidth = el.offsetWidth; + bubbleBoundingHeight = el.offsetHeight; + utils.removeClass(el, 'fade-in-down fade-in-up fade-in-left fade-in-right'); + + // SET POSITION + boundingRect = targetEl.getBoundingClientRect(); + + verticalLeftPosition = step.isRtl ? boundingRect.right - bubbleBoundingWidth : boundingRect.left; + + if (step.placement === 'top') { + top = boundingRect.top - bubbleBoundingHeight - this.opt.arrowWidth; + left = verticalLeftPosition; + } else if (step.placement === 'bottom') { + top = boundingRect.bottom + this.opt.arrowWidth; + left = verticalLeftPosition; + } else if (step.placement === 'left') { + top = boundingRect.top; + left = boundingRect.left - bubbleBoundingWidth - this.opt.arrowWidth; + } else if (step.placement === 'right') { + top = boundingRect.top; + left = boundingRect.right + this.opt.arrowWidth; + } else { + throw new Error('Bubble placement failed because step.placement is invalid or undefined!'); + } + + // SET (OR RESET) ARROW OFFSETS + if (step.arrowOffset !== 'center') { + arrowOffset = utils.getPixelValue(step.arrowOffset); + } else { + arrowOffset = step.arrowOffset; + } + if (!arrowOffset) { + arrowEl.style.top = ''; + arrowEl.style[arrowPos] = ''; + } else if (step.placement === 'top' || step.placement === 'bottom') { + arrowEl.style.top = ''; + if (arrowOffset === 'center') { + arrowEl.style[arrowPos] = Math.floor(bubbleBoundingWidth / 2 - arrowEl.offsetWidth / 2) + 'px'; + } else { + // Numeric pixel value + arrowEl.style[arrowPos] = arrowOffset + 'px'; + } + } else if (step.placement === 'left' || step.placement === 'right') { + arrowEl.style[arrowPos] = ''; + if (arrowOffset === 'center') { + arrowEl.style.top = Math.floor(bubbleBoundingHeight / 2 - arrowEl.offsetHeight / 2) + 'px'; + } else { + // Numeric pixel value + arrowEl.style.top = arrowOffset + 'px'; + } + } + + // HORIZONTAL OFFSET + if (step.xOffset === 'center') { + left = boundingRect.left + targetEl.offsetWidth / 2 - bubbleBoundingWidth / 2; + } else { + left += utils.getPixelValue(step.xOffset); + } + // VERTICAL OFFSET + if (step.yOffset === 'center') { + top = boundingRect.top + targetEl.offsetHeight / 2 - bubbleBoundingHeight / 2; + } else { + top += utils.getPixelValue(step.yOffset); + } + + // ADJUST TOP FOR SCROLL POSITION + if (!step.fixedElement) { + top += utils.getScrollTop(); + left += utils.getScrollLeft(); + } + + // ACCOUNT FOR FIXED POSITION ELEMENTS + el.style.position = step.fixedElement ? 'fixed' : 'absolute'; + + el.style.top = top + 'px'; + el.style.left = left + 'px'; + }, + + /** + * Renders the bubble according to the step JSON. + * + * @param {Object} step Information defining how the bubble should look. + * @param {Number} idx The index of the step in the tour. Not used for callouts. + * @param {Function} callback Function to be invoked after rendering is finished. + */ + render: function render(step, idx, callback) { + var el = this.element, + tourSpecificRenderer, + customTourData, + unsafe, + currTour, + totalSteps, + totalStepsI18n, + nextBtnText, + isLast, + i, + opts; + + // Cache current step information. + if (step) { + this.currStep = step; + } else if (this.currStep) { + step = this.currStep; + } + + // Check current tour for total number of steps and custom render data + if (this.opt.isTourBubble) { + currTour = winHopscotch.getCurrTour(); + if (currTour) { + customTourData = currTour.customData; + tourSpecificRenderer = currTour.customRenderer; + step.isRtl = step.hasOwnProperty('isRtl') ? step.isRtl : currTour.hasOwnProperty('isRtl') ? currTour.isRtl : this.opt.isRtl; + unsafe = currTour.unsafe; + if (Array.isArray(currTour.steps)) { + totalSteps = currTour.steps.length; + totalStepsI18n = this._getStepI18nNum(this._getStepNum(totalSteps - 1)); + isLast = this._getStepNum(idx) === this._getStepNum(totalSteps - 1); + } + } + } else { + customTourData = step.customData; + tourSpecificRenderer = step.customRenderer; + unsafe = step.unsafe; + step.isRtl = step.hasOwnProperty('isRtl') ? step.isRtl : this.opt.isRtl; + } + + // Determine label for next button + if (isLast) { + nextBtnText = utils.getI18NString('doneBtn'); + } else if (step.showSkip) { + nextBtnText = utils.getI18NString('skipBtn'); + } else { + nextBtnText = utils.getI18NString('nextBtn'); + } + + utils.flipPlacement(step); + utils.normalizePlacement(step); + + this.placement = step.placement; + + // Setup the configuration options we want to pass along to the template + opts = { + i18n: { + prevBtn: utils.getI18NString('prevBtn'), + nextBtn: nextBtnText, + closeTooltip: utils.getI18NString('closeTooltip'), + stepNum: this._getStepI18nNum(this._getStepNum(idx)), + numSteps: totalStepsI18n + }, + buttons: { + showPrev: utils.valOrDefault(step.showPrevButton, this.opt.showPrevButton) && this._getStepNum(idx) > 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 || '', + isRtl: step.isRtl, + 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 (!winHopscotch.templates || typeof winHopscotch.templates[tourSpecificRenderer] !== 'function') { + throw new Error('Bubble rendering failed - template "' + tourSpecificRenderer + '" is not a function.'); + } + el.innerHTML = winHopscotch.templates[tourSpecificRenderer](opts); + } else if (customRenderer) { + el.innerHTML = customRenderer(opts); + } else { + if (!winHopscotch.templates || typeof winHopscotch.templates[templateToUse] !== 'function') { + throw new Error('Bubble rendering failed - template "' + templateToUse + '" is not a function.'); + } + el.innerHTML = winHopscotch.templates[templateToUse](opts); + } + + // Find arrow among new child elements. + var children = el.children; + var numChildren = children.length; + var node; + 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 = typeof step.zindex === 'number' ? 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 step number considering steps that were skipped because their target wasn't found + * + * @private + */ + _getStepNum: function _getStepNum(idx) { + var skippedStepsCount = 0, + stepIdx, + skippedSteps = winHopscotch.getSkippedStepsIndexes(), + i, + len = skippedSteps.length; + //count number of steps skipped before current step + for (i = 0; i < len; i++) { + stepIdx = skippedSteps[i]; + if (stepIdx < idx) { + skippedStepsCount++; + } + } + return idx - skippedStepsCount; + }, + /** + * Get the I18N step number for the current step. + * + * @private + */ + _getStepI18nNum: function _getStepI18nNum(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 _setArrow(placement) { + 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 (placement === 'top') { + utils.addClass(this.arrowEl, 'down'); + } else if (placement === 'bottom') { + utils.addClass(this.arrowEl, 'up'); + } else if (placement === 'left') { + utils.addClass(this.arrowEl, 'right'); + } else if (placement === 'right') { + utils.addClass(this.arrowEl, 'left'); + } + }, + + /** + * @private + */ + _getArrowDirection: function _getArrowDirection() { + 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 show() { + 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 hide(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 destroy() { + var el = this.element; + + if (el) { + el.parentNode.removeChild(el); + } + utils.removeEvtListener(el, 'click', this.clickCb); + }, + + _handleBubbleClick: function _handleBubbleClick(evt) { + var action; + + // Override evt for IE8 as IE8 doesn't pass event but binds it to window + evt = evt || window.event; // get window.event if argument is falsy (in IE) + + // get srcElement if target is falsy (IE) + var targetElement = evt.target || evt.srcElement; + + //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(targetElement); + + //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 init(initOpt) { + var el = document.createElement('div'), + self = this, + resizeCooldown = false, + // for updating after window resize + onWinResize, + _appendToBody2, + children, + numChildren, + node, + i, + currTour, + 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, + isRtl: defaultOpts.isRtl, + showNumber: true, + isTourBubble: true + }; + initOpt = (typeof initOpt === 'undefined' ? 'undefined' : _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'); + } else { + currTour = winHopscotch.getCurrTour(); + if (currTour) { + utils.addClass(el, 'tour-' + currTour.id); + } + } + + /** + * 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 onWinResize() { + 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) { + _appendToBody2 = function appendToBody() { + document.removeEventListener('DOMContentLoaded', _appendToBody2); + window.removeEventListener('load', _appendToBody2); + + document.body.appendChild(el); + }; + + document.addEventListener('DOMContentLoaded', _appendToBody2, false); + } + // IE + else { + _appendToBody2 = function _appendToBody() { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', _appendToBody2); + window.detachEvent('onload', _appendToBody2); + document.body.appendChild(el); + } + }; + + document.attachEvent('onreadystatechange', _appendToBody2); + } + utils.addEvtListener(window, 'load', _appendToBody2); + } + } + }; + + /** + * HopscotchCalloutManager + * + * @class Manages the creation and destruction of single callouts. + * @constructor + */ + HopscotchCalloutManager = function HopscotchCalloutManager() { + var callouts = {}, + calloutOpts = {}; + + /** + * 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 (!validIdRegEx.test(opt.id)) { + throw new Error('Callout ID is using an invalid format. Use alphanumeric, underscores, and/or hyphens only. First character must be a letter.'); + } + if (callouts[opt.id]) { + throw new Error('Callout by that id already exists. Please choose a unique id.'); + } + if (!utils.getStepTarget(opt)) { + throw new Error('Must specify existing target element via \'target\' option.'); + } + opt.showNextButton = opt.showPrevButton = false; + opt.isTourBubble = false; + callout = new HopscotchBubble(opt); + callouts[opt.id] = callout; + calloutOpts[opt.id] = opt; + callout.render(opt, null, function () { + callout.show(); + if (opt.onShow) { + utils.invokeCallback(opt.onShow); + } + }); + } else { + throw new Error('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; + + for (calloutId in callouts) { + if (callouts.hasOwnProperty(calloutId)) { + this.removeCallout(calloutId); + } + } + }; + + /** + * removeCallout + * + * 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; + calloutOpts[id] = null; + if (!callout) { + return; + } + + callout.destroy(); + }; + + /** + * refreshCalloutPositions + * + * Refresh the positions for all callouts known by the + * callout manager. Typically you'll use + * hopscotch.refreshBubblePosition() to refresh ALL + * bubbles instead of calling this directly. + */ + this.refreshCalloutPositions = function () { + var calloutId, callout, opts; + + for (calloutId in callouts) { + if (callouts.hasOwnProperty(calloutId) && calloutOpts.hasOwnProperty(calloutId)) { + callout = callouts[calloutId]; + opts = calloutOpts[calloutId]; + if (callout && opts) { + callout.setPosition(opts); + } + } + } + }; + }; + + /** + * 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 Hopscotch(initOptions) { + var self = this, + // for targetClickNextFn + bubble, + calloutMgr, + opt, + currTour, + currStepNum, + skippedSteps = {}, + cookieTourId, + cookieTourStep, + cookieSkippedSteps = [], + _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 getBubble(setOptions) { + if (!bubble || !bubble.element || !bubble.element.parentNode) { + 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'), + isRtl: getOption('isRtl') + }); + } + return bubble; + }, + + + /** + * Destroy the bubble currently associated with Hopscotch. + * This is done when we end the current tour. + * + * @private + */ + destroyBubble = function destroyBubble() { + if (bubble) { + bubble.destroy(); + bubble = null; + } + }, + + + /** + * 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 getOption(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 getCurrStep() { + var step; + + if (!currTour || currStepNum < 0 || currStepNum >= currTour.steps.length) { + step = null; + } else { + step = currTour.steps[currStepNum]; + } + + return step; + }, + + + /** + * Used for nextOnTargetClick + * + * @private + */ + targetClickNextFn = function targetClickNextFn() { + 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 adjustWindowScroll(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 === 'undefined' ? 'undefined' : _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 scrollTimeoutFn() { + 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 goToStepWithTarget(direction, cb) { + var target, step, goToStepFn; + + if (currStepNum + direction >= 0 && currStepNum + direction < currTour.steps.length) { + + currStepNum += direction; + step = getCurrStep(); + + goToStepFn = function goToStepFn() { + target = utils.getStepTarget(step); + + if (target) { + //this step was previously skipped, but now its target exists, + //remove this step from skipped steps set + if (skippedSteps[currStepNum]) { + delete skippedSteps[currStepNum]; + } + // We're done! Return the step number via the callback. + cb(currStepNum); + } else { + //mark this step as skipped, since its target wasn't found + skippedSteps[currStepNum] = true; + // 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 changeStep(doCallbacks, direction) { + var bubble = getBubble(), + self = this, + step, + origStep, + wasMultiPage, + changeStepCb; + + bubble.hide(); + + doCallbacks = utils.valOrDefault(doCallbacks, true); + + step = getCurrStep(); + + if (step.nextOnTargetClick) { + // Detach the listener when tour is moving to a different step + utils.removeEvtListener(utils.getStepTarget(step), 'click', targetClickNextFn); + } + + origStep = step; + if (direction > 0) { + wasMultiPage = origStep.multipage; + } else { + wasMultiPage = currStepNum > 0 && currTour.steps[currStepNum - 1].multipage; + } + + /** + * Callback for goToStepWithTarget + * + * @private + */ + changeStepCb = function changeStepCb(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 + setStateHelper(); + + // 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); + } else if (currStepNum + direction === currTour.steps.length) { + return this.endTour(); + } + + return this; + }, + + + /** + * loadTour + * + * Loads, but does not display, tour. + * + * @private + * @param tour The tour JSON object + */ + loadTour = function loadTour(tour) { + var tmpOpt = {}, + prop, + tourState, + tourStateValues; + + // 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) { + tourStateValues = tourState.split(':'); + cookieTourId = tourStateValues[0]; // selecting tour is not supported by this framework. + cookieTourStep = tourStateValues[1]; + + if (tourStateValues.length > 2) { + cookieSkippedSteps = tourStateValues[2].split(','); + } + + 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 findStartingStep(startStepNum, savedSkippedSteps, cb) { + var step, target; + + currStepNum = startStepNum || 0; + skippedSteps = savedSkippedSteps || {}; + 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'); + + //this step was skipped, since its target does not exist + skippedSteps[currStepNum] = true; + + if (getOption('skipIfNoElement')) { + goToStepWithTarget(1, cb); + return; + } else { + currStepNum = -1; + cb(currStepNum); + } + } + }, + showStepHelper = function showStepHelper(stepNum) { + var step = currTour.steps[stepNum], + bubble = getBubble(), + targetEl = utils.getStepTarget(step); + + function showBubble() { + bubble.show(); + utils.invokeEventCallbacks('show', step.onShow); + } + + if (currStepNum !== stepNum && getCurrStep().nextOnTargetClick) { + // Detach the listener when tour is moving to a different step + utils.removeEvtListener(utils.getStepTarget(getCurrStep()), 'click', targetClickNextFn); + } + + // Update bubble for current step + currStepNum = stepNum; + + bubble.hide(false); + + 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); + } + }); + + setStateHelper(); + }, + setStateHelper = function setStateHelper() { + var cookieVal = currTour.id + ':' + currStepNum, + skipedStepIndexes = winHopscotch.getSkippedStepsIndexes(); + + if (skipedStepIndexes && skipedStepIndexes.length > 0) { + cookieVal += ':' + skipedStepIndexes.join(','); + } + + utils.setState(getOption('cookieName'), cookieVal, 1); + }, + + + /** + * init + * + * Initializes the Hopscotch object. + * + * @private + */ + init = function init(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 === 'undefined' ? 'undefined' : _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, + skippedSteps = {}, + self = this; + + // loadTour if we are calling startTour directly. (When we call startTour + // from window onLoad handler, we'll use currTour) + if (!currTour) { + + // Sanity check! Is there a tour? + if (!tour) { + throw new Error('Tour data is required for startTour.'); + } + + // Check validity of tour ID. If invalid, throw an error. + if (!tour.id || !validIdRegEx.test(tour.id)) { + throw new Error('Tour ID is using an invalid format. Use alphanumeric, underscores, and/or hyphens only. First character must be a letter.'); + } + + currTour = tour; + loadTour.call(this, tour); + } + + if ((typeof stepNum === 'undefined' ? 'undefined' : _typeof(stepNum)) !== undefinedStr) { + if (stepNum >= currTour.steps.length) { + throw new Error('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 === 'undefined' ? 'undefined' : _typeof(cookieTourStep)) !== undefinedStr) { + currStepNum = cookieTourStep; + if (cookieSkippedSteps.length > 0) { + for (var i = 0, len = cookieSkippedSteps.length; i < len; i++) { + skippedSteps[cookieSkippedSteps[i]] = true; + } + } + } else if (!currStepNum) { + currStepNum = 0; + } + + // Find the current step we should begin the tour on, and then actually start the tour. + findStartingStep(currStepNum, skippedSteps, 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], + prevStepNum = currStepNum; + if (!utils.getStepTarget(step)) { + currStepNum = stepNum; + utils.invokeEventCallbacks('error'); + currStepNum = prevStepNum; + return; + } + + 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) { + 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(), + currentStep; + + clearState = utils.valOrDefault(clearState, true); + doCallbacks = utils.valOrDefault(doCallbacks, true); + + //remove event listener if current step had it added + if (currTour) { + currentStep = getCurrStep(); + if (currentStep && currentStep.nextOnTargetClick) { + utils.removeEvtListener(utils.getStepTarget(currentStep), 'click', targetClickNextFn); + } + } + + 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(); + destroyBubble(); + + 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; + }; + + /** + * getSkippedStepsIndexes + * + * @return {Array} Array of skipped step indexes + */ + this.getSkippedStepsIndexes = function () { + var skippedStepsIdxArray = [], + stepIds; + + for (stepIds in skippedSteps) { + skippedStepsIdxArray.push(stepIds); + } + + return skippedStepsIdxArray; + }; + + /** + * refreshBubblePosition + * + * Tell hopscotch that the position of the current tour element changed + * and the bubble therefore needs to be redrawn. Also refreshes position + * of all Hopscotch Callouts on the page. + * + * @returns {Object} Hopscotch + */ + this.refreshBubblePosition = function () { + var currStep = getCurrStep(); + if (currStep) { + getBubble().setPosition(currStep); + } + this.getCalloutManager().refreshCalloutPositions(); + 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 === 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 _configure(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.
+     * - isRtl:           Boolean  - Set to true when instantiating in a right-to-left
+     *                               language environment, or if mirrored positioning is
+     *                               needed.
+     *                               Defaults to FALSE.
+     *
+     * - 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 === 'undefined' ? 'undefined' : _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(); + + // 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["templates"] = this["templates"] || {}; + +this["templates"]["bubble_default"] = function(data) { +var __t, __p = '', __e = _.escape, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } + + + function optEscape(str, unsafe){ + if(unsafe){ + return _.escape(str); + } + return str; + } +; +__p += '\n'; + +var i18n = data.i18n; +var buttons = data.buttons; +var step = data.step; +var tour = data.tour; +; +__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 += ''; + } ; +__p += '\n
\n
\n
\n
\n
\n'; +return __p +}; + }).call(winHopscotch); + + var winHopscotch$1 = winHopscotch; + + return winHopscotch$1; + +}); diff --git a/dist/js/hopscotch_amd.min.js b/dist/js/hopscotch_amd.min.js new file mode 100644 index 00000000..4f55e978 --- /dev/null +++ b/dist/js/hopscotch_amd.min.js @@ -0,0 +1,17 @@ +/**! hopscotch - v0.3.0 +* +* Copyright 2017 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. +*/ +define("hopscotch",function(){"use strict";var a,b,c,d,e,f,g,h,i,j,k,l,m,n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},o="bubble_default",p=window.Sizzle||null,q="undefined",r=!1,s=("undefined"==typeof jQuery?"undefined":n(jQuery))!==q,t=!1,u=!1,v=/^[a-zA-Z]+[a-zA-Z0-9_-]*$/,w={left:"right",right:"left"};try{n(window.sessionStorage)!==q&&(t=!0,sessionStorage.setItem("hopscotch.test.storage","ok"),sessionStorage.removeItem("hopscotch.test.storage"),u=!0)}catch(x){}l={smoothScroll:!0,scrollDuration:1e3,scrollTopMargin:200,showCloseButton:!0,showPrevButton:!1,showNextButton:!0,bubbleWidth:280,bubblePadding:15,arrowWidth:20,skipIfNoElement:!0,isRtl:!1,cookieName:"hopscotch.tour.state"},Array.isArray||(Array.isArray=function(a){return"[object Array]"===Object.prototype.toString.call(a)}),k=function(){r&&m.startTour()},h={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="undefined"==typeof a?"undefined":n(a);return"number"===b?a:"string"===b?parseInt(a,10):0},valOrDefault:function(a,b){return("undefined"==typeof a?"undefined":n(a))!==q?a:b},invokeCallbackArrayHelper:function(a){var b;return Array.isArray(a)&&(b=j[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 h.invokeCallbackArrayHelper(a);for(b=0,c=a.length;c>b;++b)h.invokeCallback(a[b])}},invokeCallback:function(a){return"function"==typeof a?a():"string"==typeof a&&j[a]?j[a]():h.invokeCallbackArray(a)},invokeEventCallbacks:function(a,b){var c,d,e=i[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=n(window.pageYOffset)!==q?window.pageYOffset:document.documentElement.scrollTop},getScrollLeft:function(){var a;return a=n(window.pageXOffset)!==q?window.pageXOffset:document.documentElement.scrollLeft},getWindowHeight:function(){return window.innerHeight||document.documentElement.clientHeight},addEvtListener:function(a,b,c){return a?a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c):void 0},removeEvtListener:function(a,b,c){return a?a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c):void 0},documentIsReady:function(){return"complete"===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);if(b)return b;if(s)return b=jQuery(a),b.length?b[0]:null;if(p)return b=new p(a),b.length?b[0]:null;if(document.querySelector)try{return document.querySelector(a)}catch(c){}return/^#[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 h.getStepTargetHelper(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=h.getStepTargetHelper(a.target[c])))return b;return null}return a.target},getI18NString:function(a){return e[a]||d[a]},setState:function(a,b,c){var d,e="";if(t&&u)try{sessionStorage.setItem(a,b)}catch(f){u=!1,this.setState(a,b,c)}else t&&sessionStorage.removeItem(a),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(t&&(d=sessionStorage.getItem(a)))return d;for(b=0;b0,showNext:h.valOrDefault(a.showNextButton,this.opt.showNextButton),showCTA:h.valOrDefault(a.showCTAButton&&a.ctaLabel,!1),ctaLabel:a.ctaLabel,showClose:h.valOrDefault(this.opt.showCloseButton,!0)},step:{num:b,isLast:h.valOrDefault(n,!1),title:a.title||"",content:a.content||"",isRtl:a.isRtl,placement:a.placement,padding:h.valOrDefault(a.padding,this.opt.bubblePadding),width:h.getPixelValue(a.width)||this.opt.bubbleWidth,customData:a.customData||{}},tour:{isTour:this.opt.isTourBubble,numSteps:j,unsafe:h.valOrDefault(g,!1),customData:e||{}}},"function"==typeof d)r.innerHTML=d(q);else if("string"==typeof d){if(!m.templates||"function"!=typeof m.templates[d])throw new Error('Bubble rendering failed - template "'+d+'" is not a function.');r.innerHTML=m.templates[d](q)}else if(f)r.innerHTML=f(q);else{if(!m.templates||"function"!=typeof m.templates[o])throw new Error('Bubble rendering failed - template "'+o+'" is not a function.');r.innerHTML=m.templates[o](q)}var s,t=r.children,u=t.length;for(p=0;u>p;p++)s=t[p],h.hasClass(s,"hopscotch-arrow")&&(this.arrowEl=s);return r.style.zIndex="number"==typeof a.zindex?a.zindex:"",this._setArrow(a.placement),this.hide(!1),this.setPosition(a),c&&c(!a.fixedElement),this},_getStepNum:function(a){var b,c,d=0,e=m.getSkippedStepsIndexes(),f=e.length;for(c=0;f>c;c++)b=e[c],a>b&&d++;return a-d},_getStepI18nNum:function(a){var b=h.getI18NString("stepNums");return b&&au||u>=t.steps.length?null:t.steps[u]},G=function(){z.nextStep()},H=function(a){var b,c,d,e,f,g,i=C(),j=i.element,k=h.getPixelValue(j.style.top),l=k+h.getPixelValue(j.offsetHeight),m=h.getStepTarget(F()),o=m.getBoundingClientRect(),p=o.top+h.getScrollTop(),r=o.bottom+h.getScrollTop(),t=p>k?k:p,u=l>r?l:r,v=h.getScrollTop(),w=v+h.getWindowHeight(),x=t-E("scrollTopMargin");t>=v&&(t<=v+E("scrollTopMargin")||w>=u)?a&&a():E("smoothScroll")?("undefined"==typeof YAHOO?"undefined":n(YAHOO))!==q&&n(YAHOO.env)!==q&&n(YAHOO.env.ua)!==q&&n(YAHOO.util)!==q&&n(YAHOO.util.Scroll)!==q?(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,x]}},E("scrollDuration")/1e3,d),c.onComplete.subscribe(a),c.animate()):s?jQuery("body, html").animate({scrollTop:x},E("scrollDuration"),a):(0>x&&(x=0),e=v>t?-1:1,f=Math.abs(v-x)/(E("scrollDuration")/10),(g=function(){var b=h.getScrollTop(),c=b+e*f;return e>0&&c>=x||0>e&&x>=c?(c=x,a&&a(),void window.scrollTo(0,c)):(window.scrollTo(0,c),h.getScrollTop()===b?void(a&&a()):void setTimeout(g,10))})()):(window.scrollTo(0,x),a&&a())},I=function P(a,b){var c,d,e;u+a>=0&&u+a0?d.multipage:u>0&&t.steps[u-1].multipage,f=function(c){var f;if(-1===c)return this.endTour(!0);if(a&&(f=b>0?h.invokeEventCallbacks("next",d.onNext):h.invokeEventCallbacks("prev",d.onPrev)),c===u){if(e)return void N();f=h.valOrDefault(f,!0),f?this.showStep(c):this.endTour(!1)}},!e&&E("skipIfNoElement"))I(b,function(a){f.call(i,a)});else if(u+b>=0&&u+b2&&(B=d[2].split(",")),x=parseInt(x,10)),this},L=function(a,b,c){var d,e;if(u=a||0,A=b||{},d=F(),e=h.getStepTarget(d))return void c(u);if(!e){if(h.invokeEventCallbacks("error"),A[u]=!0,E("skipIfNoElement"))return void I(1,c);u=-1,c(u)}},M=function(a){function b(){d.show(),h.invokeEventCallbacks("show",c.onShow)}var c=t.steps[a],d=C(),e=h.getStepTarget(c);u!==a&&F().nextOnTargetClick&&h.removeEvtListener(h.getStepTarget(F()),"click",G),u=a,d.hide(!1),d.render(c,a,function(a){a?H(b):b(),c.nextOnTargetClick&&h.addEvtListener(e,"click",G)}),N()},N=function(){var a=t.id+":"+u,b=m.getSkippedStepsIndexes();b&&b.length>0&&(a+=":"+b.join(",")),h.setState(E("cookieName"),a,1)},O=function(a){a&&this.configure(a)};this.getCalloutManager=function(){return("undefined"==typeof k?"undefined":n(k))===q&&(k=new c),k},this.startTour=function(a,b){var c,d,e={},f=this;if(!t){if(!a)throw new Error("Tour data is required for startTour.");if(!a.id||!v.test(a.id))throw new Error("Tour ID is using an invalid format. Use alphanumeric, underscores, and/or hyphens only. First character must be a letter.");t=a,K.call(this,a)}if(("undefined"==typeof b?"undefined":n(b))!==q){if(b>=t.steps.length)throw new Error("Specified step number out of bounds.");d=b}if(!h.documentIsReady())return r=!0,this;if("undefined"==typeof d&&t.id===w&&("undefined"==typeof x?"undefined":n(x))!==q){if(d=x,B.length>0)for(var g=0,i=B.length;i>g;g++)e[B[g]]=!0}else d||(d=0);return L(d,e,function(a){var b=-1!==a&&h.getStepTarget(t.steps[a]);return b?(h.invokeEventCallbacks("start"),c=C(),c.hide(!1),f.isActive=!0,void(h.getStepTarget(F())?f.showStep(a):(h.invokeEventCallbacks("error"),E("skipIfNoElement")&&f.nextStep(!1)))):void f.endTour(!1,!1)}),this},this.showStep=function(a){var b=t.steps[a],c=u;return h.getStepTarget(b)?(b.delay?setTimeout(function(){M(a)},b.delay):M(a),this):(u=a,h.invokeEventCallbacks("error"),void(u=c))},this.prevStep=function(a){return J.call(this,a,-1),this},this.nextStep=function(a){return J.call(this,a,1),this},this.endTour=function(a,b){var c,d=C();return a=h.valOrDefault(a,!0),b=h.valOrDefault(b,!0),t&&(c=F(),c&&c.nextOnTargetClick&&h.removeEvtListener(h.getStepTarget(c),"click",G)),u=0,x=void 0,d.hide(),a&&h.clearState(E("cookieName")),this.isActive&&(this.isActive=!1,t&&b&&h.invokeEventCallbacks("end")),this.removeCallbacks(null,!0),this.resetDefaultOptions(),D(),t=null,this},this.getCurrTour=function(){return t},this.getCurrTarget=function(){return h.getStepTarget(F())},this.getCurrStepNum=function(){return u},this.getSkippedStepsIndexes=function(){var a,b=[];for(a in A)b.push(a);return b},this.refreshBubblePosition=function(){var a=F();return a&&C().setPosition(a),this.getCalloutManager().refreshCalloutPositions(),this},this.listen=function(a,b,c){return a&&i[a].push({cb:b,fromTour:c}),this},this.unlisten=function(a,b){var c,d,e=i[a];for(c=0,d=e.length;d>c;++c)e[c].cb===b&&e.splice(c,1);return this},this.removeCallbacks=function(a,b){var c,d,e,f;for(f in i)if(!a||a===f)if(b)for(c=i[f],d=0,e=c.length;e>d;++d)c[d].fromTour&&(c.splice(d--,1),--e);else i[f]=[];return this},this.registerHelper=function(a,b){"string"==typeof a&&"function"==typeof b&&(j[a]=b)},this.unregisterHelper=function(a){j[a]=null},this.invokeHelper=function(a){var b,c,d=[];for(b=1,c=arguments.length;c>b;++b)d.push(arguments[b]);j[a]&&j[a].call(null,d)},this.setCookieName=function(a){return p.cookieName=a,this},this.resetDefaultOptions=function(){return p={},this},this.resetDefaultI18N=function(){return e={},this},this.getState=function(){return h.getState(E("cookieName"))},y=function(a,b){var c,d,f,g,i=["next","prev","start","end","show","error","close"];for(p||this.resetDefaultOptions(),h.extend(p,a),a&&h.extend(e,a.i18n),f=0,g=i.length;g>f;++f)d="on"+i[f].charAt(0).toUpperCase()+i[f].substring(1),a[d]&&this.listen(i[f],a[d],b);return c=C(!0),this},this.configure=function(a){return y.call(this,a,!1)},this.setRenderer=function(a){var b="undefined"==typeof a?"undefined":n(a);return"string"===b?(o=a,f=void 0):"function"===b&&(f=a),this},this.setEscaper=function(a){return"function"==typeof a&&(g=a),this},O.call(this,a)},m=new a,function(){var a={};a.escape=function(a){return g?g(a):null==a?"":(""+a).replace(new RegExp("[&<>\"']","g"),function(a){return"&"==a?"&":"<"==a?"<":">"==a?">":'"'==a?""":"'"==a?"'":void 0})},this.templates=this.templates||{},this.templates.bubble_default=function(b){function c(b,c){return c?a.escape(b):b}var d,e="";a.escape,Array.prototype.join;e+="\n";var f=b.i18n,g=b.buttons,h=b.step,i=b.tour;return e+='\n
\n ',i.isTour&&(e+=''+(null==(d=f.stepNum)?"":d)+""),e+='\n
\n ',""!==h.title&&(e+='

'+(null==(d=c(h.title,i.unsafe))?"":d)+"

"),e+="\n ",""!==h.content&&(e+='
'+(null==(d=c(h.content,i.unsafe))?"":d)+"
"),e+='\n
\n
\n ',g.showPrev&&(e+='"),e+="\n ",g.showCTA&&(e+='"),e+="\n ",g.showNext&&(e+='"),e+="\n
\n ",g.showClose&&(e+='"),e+='\n
\n
\n
\n
\n
\n'}}.call(m);var y=m;return y}); \ No newline at end of file