<NME> pageguide.js
<BEF> /*
 * Tracelytics PageGuide
 *
 * Copyright 2013 Tracelytics
 * Free to use under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Contributing Author: Tracelytics Team
 */

/*
 * PageGuide usage:
 *
 *  Preferences:
 *  auto_show_first:    Whether or not to focus on the first visible item
 *                      immediately on PG open (default true)
 *  loading_selector:   The CSS selector for the loading element. pageguide
 *                      will wait until this element is no longer visible
 *                      starting up.
 *  track_events_cb:    Optional callback for tracking user interactions
 *                      with pageguide.  Should be a method taking a single
 *                      parameter indicating the name of the interaction.
 *                      (default none)
 *  handle_doc_switch:  Optional callback to enlight or adapt interface
 *                      depending on current documented element. Should be a
 *                      function taking 2 parameters, current and previous
 *                      data-tourtarget selectors. (default null)
 *  custom_open_button: Optional id for toggling pageguide. Default null.
 *                      If not specified then the default button is used.
 *  pg_caption:         Optional - Sets the visible caption
 *  dismiss_welcome:    Optional function to permanently dismiss the welcome
 *                      message, corresponding to check_welcome_dismissed.
 *                      Default: sets a localStorage or cookie value for the
 *                      (hashed) current URL to indicate the welcome message
 *                      has been dismissed, corresponds to default
 *                      check_welcome_dismissed function.
 *  check_welcome_dismissed: Optional function to check whether or not the
 *                      welcome message has been dismissed. Must return true
 *                      or false. This function should check against whatever
 *                      state change is made in dismiss_welcome. Default:
 *                      checks whether a localStorage or cookie value has been
 *                      set for the (hashed) current URL, corresponds to default
 *                      dismiss_welcome function.
 *  ready_callback:     A function to run once the pageguide ready event fires.
 *  pointer_fallback:   Specify whether or not to provide a fallback for css
 *                      pointer-events in browsers that do not support it
 *                      (default true).
 *  default_zindex:     The css z-index to apply to the tlypageguide_shadow
 *                      overlay elements (default 100);
 *  steps_element:      Selector for the ul element whose steps you wish to use
 *                      in this particular pageguide object (default '#tlyPageGuide');
 *  auto_refresh:       If set to true, pageguide will run a timer to constantly
 *                      monitor the DOM for changes in the target elements and
 *                      adjust the pageguide display (bubbles, overlays, etc)
 *                      accordingly. The timer will only run while pageguide is open.
 *                      Useful for single-page or heavily dynamic apps where
 *                      pageguide steps or visible DOM elements can change often.
 *                      (default false)
 *  welcome_refresh:    Similar to auto_refresh, welcome_refresh enables a timer to
 *                      monitor the DOM for new .tlyPageGuideWelcome elements. This is
 *                      useful if your welcome element isn't loaded immediately, or if
 *                      you want to show different welcome elements on different pages.
 *                      The timer will run constantly, whether or not the pageguide is
 *                      open, so enable at your discretion. (default false)
 *  refresh_interval:   If auto_refresh or welcome_refresh is enabled, refresh_interval
 *                      indicates in ms how often to poll the DOM for changes. (default 500)
 *
 */
tl = window.tl || {};
tl.pg = tl.pg || {};
tl.pg.pageGuideList = tl.pg.pageGuideList || [];
tl.pg.interval = {};

(function ($) {
    /**
     * default preferences. can be overridden by user settings passed into
     * tl.pg.init().
     **/
    tl.pg.default_prefs = {
        'auto_show_first': true,
        'loading_selector' : '#loading',
        'track_events_cb': function() { return; },
        'handle_doc_switch': null,
        'custom_open_button': null,
        'pg_caption' : 'page guide',
        'tourtitle': 'Open Page Guide for help',
        'check_welcome_dismissed': function () {
            var key = 'tlypageguide_welcome_shown_' + tl.pg.hashUrl();
            // first, try to use localStorage
            try {
                if (localStorage.getItem(key)) {
                    return true;
                }
            // cookie fallback for older browsers
            } catch(e) {
                if (document.cookie.indexOf(key) > -1) {
                    return true;
                }
            }
            return false;
        },
        'dismiss_welcome': function () {
            var key = 'tlypageguide_welcome_shown_' + tl.pg.hashUrl();
            try {
                localStorage.setItem(key, true);
            } catch(e) {
                var exp = new Date();
                exp.setDate(exp.getDate() + 365);
                document.cookie = (key + '=true; expires=' + exp.toUTCString());
            }
        },
        'ready_callback': null,
        'pointer_fallback': true,
        'default_zindex': 100,
        'steps_element': '#tlyPageGuide',
        'auto_refresh': false,
        'refresh_welcome': false,
        'refresh_interval': 500
    };

    // boilerplate markup for the message display element and shadow/index bubble container.
    tl.pg.wrapper_markup =
        '<div id="tlyPageGuideWrapper">' +
            '<div id="tlyPageGuideOverlay"></div>' +
            '<div id="tlyPageGuideMessages">' +
                '<a href="#" class="tlypageguide_close" title="Close Guide">close</a>' +
                '<span class="tlypageguide_index"></span>' +
                '<div class="tlypageguide_text"></div>' +
                '<a href="#" class="tlypageguide_back" title="Previous">Previous</a>' +
                '<a href="#" class="tlypageguide_fwd" title="Next">Next</a>' +
            '</div>' +
            '<div id="tlyPageGuideContent"></div>' +
            '<div id="tlyPageGuideToggles"></div>' +
        '</div>';

    // boilerplate markup for the toggle element.
    tl.pg.toggle_markup =
        '<div class="tlypageguide_toggle" title="Launch Page Guide">' +
            '<div><span class="tlypageguide_toggletitle"></span></div>' +
            '<a href="#" class="tlypageguide_close" title="close guide">close guide &raquo;</a>' +
        '</div>';

    /**
     * initiates the pageguide using the given preferences. must be idempotent, that is,
     * able to run multiple times without changing state.
     * preferences (object): any preferences the user wishes to override.
     **/
    tl.pg.init = function(preferences) {
        preferences = $.extend({}, tl.pg.default_prefs, preferences);
        var $guide = $(preferences.steps_element);
        var uuid = tl.pg.hashCode(preferences.steps_element);
        clearInterval(tl.pg.interval[uuid]);

        /* page guide object, for pages that have one */
        if ($guide.length === 0) {
            return;
        }

        // only worry about pointer_fallback if pointers are not supported in
        // the user's browser
        if (preferences.pointer_fallback && tl.pg.pointerEventSupport()) {
            preferences.pointer_fallback = false;
        }

        var $wrapper = $('#tlyPageGuideWrapper');
        var wrapperExists = true;
        if (!$wrapper.length) {
            wrapperExists = false;
            $wrapper = $(tl.pg.wrapper_markup);
        }

        if (preferences.custom_open_button == null &&
            $('#tlyPageGuideToggle' + uuid).length < 1) {
            var tourtitle = $guide.data('tourtitle') || preferences.tourtitle;
            var $toggle = $(tl.pg.toggle_markup)
                .attr('id', ('tlyPageGuideToggle' + uuid))
                .prepend(preferences.pg_caption);

            $toggle.find('.tlypageguide_toggletitle').text(tourtitle);
            $wrapper.find('#tlyPageGuideToggles').append($toggle);
        }

        if (!wrapperExists) {
            $('body').prepend($wrapper);
        }

        var pg = new tl.pg.PageGuide($('#tlyPageGuideWrapper'), preferences);

        pg.ready(function() {
            pg.setup_welcome();
            // start (neverending) welcome watch timer if preference is enabled
            if (pg.preferences.welcome_refresh) {
                pg.updateTimer(function () {
                    pg.setup_welcome();
                }, 'welcome');
            }
            pg.setup_handlers();
            pg.$base.find(".tlypageguide_toggle").animate({ "right": "-120px" }, 250);
            if (typeof(preferences.ready_callback) === 'function') {
                preferences.ready_callback();
            }
        });
        tl.pg.pageGuideList.push(pg);
        return pg;
    };

    /**
     * constructor for the base PageGuide object. contains: relevant elements,
     * user-defined preferences, and state information. all of this data is public.
     * pg_elem (jQuery element): the base wrapper element which contains all the pg
     *     elements
     * preferences (object): combined user-defined and default preferences.
     **/
    tl.pg.PageGuide = function (pg_elem, preferences) {
        this.preferences = preferences;
        this.$base = pg_elem;
        this.$message = this.$base.find('#tlyPageGuideMessages');
        this.$fwd = this.$base.find('a.tlypageguide_fwd');
        this.$back = this.$base.find('a.tlypageguide_back');
        this.$content = this.$base.find('#tlyPageGuideContent');
        this.$steps = $(preferences.steps_element);
        this.uuid = tl.pg.hashCode(preferences.steps_element);
        this.$toggle = this.$base.find('#tlyPageGuideToggle' + this.uuid);
        this.cur_idx = 0;
        this.cur_selector = null;
        this.track_event = this.preferences.track_events_cb;
        this.handle_doc_switch = this.preferences.handle_doc_switch;
        this.custom_open_button = this.preferences.custom_open_button;
        this.is_open = false;
        this.targetData = {};
        this.hashTable = {};
        this.changeQueue = [];
        this.visibleTargets = [];
        this.timer = {
            overlay: null,
            welcome: null
        };
    };

    /**
     * hash the current page's url. used in the default check_welcome_dismissed
     * and dismiss_welcome functions
     **/
    tl.pg.hashUrl = function () {
        return tl.pg.hashCode(window.location.href);
    };

    /**
     * str (string): the string to be hashed
     **/
    tl.pg.hashCode = function (str) {
        var hash = 0, i, char;
        if (str == null || str.length === 0) {
            return hash;
        }
        for (i = 0; i < str.length; i++) {
            char = str.charCodeAt(i);
            hash = ((hash<<5)-hash)+char;
            hash = hash & hash;
        }
        return hash.toString();
        }
        return hash.toString();
    };

    /**
     * check whether the element targeted by the given selector is within the
     * currently scrolled viewport.
     * elem (string): selector for the element in question
     **/
    tl.pg.isScrolledIntoView = function(elem, height) {
        var dvtop = $(window).scrollTop(),
            dvbtm = dvtop + $(window).height(),
            eltop = $(elem).offset().top,
            elbtm = eltop + $(elem).height();

        return (eltop >= dvtop) && (elbtm <= dvbtm - height);
    };

    /**
     * remove all traces of pageguide from the DOM.
     **/
    tl.pg.destroy = function () {
        $('#tlyPageGuideWrapper').remove();
        $('body').removeClass('tlypageguide-open');
        $('body').removeClass('tlyPageGuideWelcomeOpen');
        for (var k in tl.pg.interval) {
            if (tl.pg.interval.hasOwnProperty(k)) {
                clearInterval(tl.pg.interval[k]);
            }
        }
    };

    /**
     * check whether pointer events are supported in the user's browser.
     * from http://stackoverflow.com/a/8898475/1135244
     **/
    tl.pg.pointerEventSupport = function () {
        var element = document.createElement('x');
        var documentElement = document.documentElement;
        var getComputedStyle = window.getComputedStyle;
        var supports = null;
        if(!('pointerEvents' in element.style)){
            return false;
        }
        element.style.pointerEvents = 'auto';
        element.style.pointerEvents = 'x';
        documentElement.appendChild(element);
        supports = getComputedStyle && getComputedStyle(element, '').pointerEvents === 'auto';
        documentElement.removeChild(element);
        return !!supports;
    };

    /**
     * close any other open pageguides
     * uuid (string): the uuid of the pageguide that should remain open
     **/
    tl.pg.closeOpenGuides = function (uuid) {
        for (var i=0; i<tl.pg.pageGuideList.length; i++) {
            if (tl.pg.pageGuideList[i].uuid !== uuid) {
                tl.pg.pageGuideList[i].close();
            }
        }
    }

    /**
     * check for a welcome message. if it exists, determine whether or not to show it,
     * using self.preferences.check_welcome_dismissed. then, bind relevant handlers to
     * the buttons included in the welcome message element.
     **/
    tl.pg.PageGuide.prototype.setup_welcome = function () {
        var $welcome = $('.tlyPageGuideWelcome, #tlyPageGuideWelcome')
            .not('#tlyPageGuideWrapper > .tlyPageGuideWelcome, #tlyPageGuideWrapper > #tlyPageGuideWelcome')
            .eq(0);
        var self = this;
        if ($welcome.length > 0) {
            self.preferences.show_welcome = !self.preferences.check_welcome_dismissed();
            if (self.preferences.show_welcome) {
                $welcome.appendTo(self.$base);
            }

            if ($welcome.find('.tlypageguide_ignore').length) {
                $welcome.on('click', '.tlypageguide_ignore', function () {
                    self.close_welcome();
                    self.track_event('PG.ignoreWelcome');
                });
            }
            if ($welcome.find('.tlypageguide_dismiss').length) {
                $welcome.on('click', '.tlypageguide_dismiss', function () {
                    self.close_welcome();
                    self.preferences.dismiss_welcome();
                    self.track_event('PG.dismissWelcome');
                });
            }
            $welcome.on('click', '.tlypageguide_start', function () {
                self.open();
                self.track_event('PG.startFromWelcome');
            });

            if (self.preferences.show_welcome) {
                self.pop_welcome();
            }
        }
    };

    /**
     * timer function. will poll the DOM at 250ms intervals until the user-defined
     * self.preferences.loading_selector becomes visible, at which point it will
     * execute the given callback. useful in cases where the DOM elements pageguide
     * depends on are loaded asynchronously.
     * callback (function): executes when loading selector is visible
     **/
    tl.pg.PageGuide.prototype.ready = function(callback) {
        var self = this;
        tl.pg.interval[self.uuid] = window.setInterval(function() {
                if (!$(self.preferences.loading_selector).is(':visible')) {
                    callback();
                    clearInterval(tl.pg.interval[self.uuid]);
                }
            }, 250);
        return this;
    };

    /**
     * grab any pageguide steps on the page that have not yet been added
     * to the pg object. for each one, append a shadow element and corresponding
     * index bubble to #tlyPageGuideContent.
     **/
    tl.pg.PageGuide.prototype.addSteps = function () {
        var self = this;
        self.$steps.find('li').each(function (i, el) {
            var $el = $(el);
            var tourTarget = $el.data('tourtarget');
            var positionClass = $el.attr('class');
            if (self.targetData[tourTarget] == null) {
                self.targetData[tourTarget] = {
                    targetStyle: {},
                    content: $el.html()
                };
                var hashCode = tl.pg.hashCode(tourTarget) + '';
                self.hashTable[hashCode] = tourTarget;
                self.$content.append(
                    '<div class="tlypageguide_shadow tlypageguide_shadow' + hashCode +
                    '" data-selectorhash="' + hashCode + '">' +
                        '<span class="tlyPageGuideStepIndex ' + positionClass +'"></span>' +
                    '</div>'
                );
            }
        });
    };

    /**
     * go through all the current targets and check whether the elements are
     * on the page and visible. if so, record all appropriate css data in self.targetData.
     * any changes in each self.targetData element get pushed to self.changeQueue.
     **/
    tl.pg.PageGuide.prototype.checkTargets = function () {
        var self = this;
        var visibleIndex = 0;
        var newVisibleTargets = [];
        for (var target in self.targetData) {
            var $elements = $(target);
            var $el;
            // assume all invisible
            var newTargetData = {
                targetStyle: {
                    display: 'none'
                }
            };
            // find first visible instance of target selector per issue #4798
            for(var i = 0; i < $elements.length; i++){
                if($($elements[i]).is(':visible') ){
                    $el = $($elements[i]); // is it weird to '$($x)'?
                    newTargetData.targetStyle.display = 'block';
                    var offset = $el.offset();
                    $.extend(newTargetData.targetStyle, {
                        top: offset.top,
                        left: offset.left,
                        width: $el.outerWidth(),
                        height: $el.outerHeight(),
                        'z-index': $el.css('z-index')
                    });
                    visibleIndex++;
                    newTargetData.index = visibleIndex;
                    newVisibleTargets.push(target);
                    break;
                }
            }
            var diff = {
                target: target
            };
            // compare new styles with existing ones
            for (var prop in newTargetData.targetStyle) {
                if (newTargetData.targetStyle[prop] !== self.targetData[target][prop]) {
                    if (diff.targetStyle == null) {
                        diff.targetStyle = {};
                    }
                    diff.targetStyle[prop] = newTargetData.targetStyle[prop];
                }
            }
            // compare index with existing index
            if (newTargetData.index !== self.targetData[target].index) {
                diff.index = newTargetData.index;
            }
            // push diff onto changequeue if changes have been made
            if (diff.targetStyle != null || diff.index != null) {
                self.changeQueue.push(diff);
            }
            $.extend(self.targetData[target], newTargetData);
        }
        self.visibleTargets = newVisibleTargets;
    };

    /**
     * position the shadow elements (and their attached index bubbles) in their
     * appropriate places over the visible targets. executes by iterating through
     * all the changes that have been pushed to changeQueue
     **/
    tl.pg.PageGuide.prototype.positionOverlays = function () {
        for (var i=0; i<this.changeQueue.length; i++) {
            var changes = this.changeQueue[i];
            var selector = '.tlypageguide_shadow' + tl.pg.hashCode(changes.target);
            var $el = this.$content.find(selector);
            if (changes.targetStyle != null) {
                var style = $.extend({}, changes.targetStyle);
                for (var prop in style) {
                    // fix this
                    if (prop === 'z-index') {
                        style[prop] = (typeof style[prop] === 'number') ?
                            style[prop] + 1 : this.preferences.default_zindex;
                    }
                }
                $el.css(style);
            }
            if (changes.index != null) {
                $el.find('.tlyPageGuideStepIndex').text(changes.index);
            }
        }
        this.changeQueue = [];
    };

    /**
     * find all pageguide steps and appropriately position their corresponding pageguide
     * elements. ideal to run on its own whenever pageguide is opened, or when a DOM
     * change takes place that will not affect the visibility of the target elements
     * (e.g. resize)
     **/
    tl.pg.PageGuide.prototype.refreshVisibleSteps = function () {
        this.addSteps();
        this.checkTargets();
        this.positionOverlays();
    };

    /**
     * update visible steps on page, and also navigate to the next available step if
     * necessary. this is especially useful when DOM changes take place while the
     * pageguide is open, meaning its target elements may be affected.
     **/
    tl.pg.PageGuide.prototype.updateVisible = function () {
        this.refreshVisibleSteps();
        if (this.cur_selector != null && this.cur_selector !== this.visibleTargets[this.cur_idx]) {
            // mod by target length in case user was viewing last target and it got removed
            var newIndex = this.cur_idx % this.visibleTargets.length;
            this.show_message(newIndex);
        }
    };

    /**
     * show the step specified by either a numeric index or a selector.
     * index (number): index of the currently visible step to show.
     **/
    tl.pg.PageGuide.prototype.show_message = function (index) {
        var targetKey = this.visibleTargets[index];
        var target = this.targetData[targetKey];
        if (target != null) {
            var selector = '.tlypageguide_shadow' + tl.pg.hashCode(targetKey);

            if (this.handle_doc_switch) {
                var len = this.visibleTargets.length;
                var prevTargetKey = this.visibleTargets[(index - 1 + len) % len];
                this.handle_doc_switch(targetKey, prevTargetKey);
            }

            this.$content.find('.tlypageguide-active').removeClass('tlypageguide-active');
            this.$content.find(selector).addClass('tlypageguide-active');

            this.$message.find('.tlypageguide_text').html(target.content);
            this.cur_idx = index;
            this.cur_selector = targetKey;

            // DOM stuff
            var defaultHeight = 100;
            var oldHeight = parseFloat(this.$message.css("height"));
            this.$message.css("height", "auto");
            var height = parseFloat(this.$message.outerHeight());
            this.$message.css("height", oldHeight);
            if (height < defaultHeight) {
                height = defaultHeight;
            }
            if (height > $(window).height()/2) {
                height = $(window).height()/2;
            }

            this.$message.show().animate({'height': height}, 500);
            if (!tl.pg.isScrolledIntoView($(targetKey), this.$message.outerHeight())) {
                $('html,body').animate({scrollTop: target.targetStyle.top - 50}, 500);
            }
            this.roll_number(this.$message.find('span'), target.index);
        }
    };

    /**
     * navigate to the previous step. if at the first step, loop around to the last.
     **/
    tl.pg.PageGuide.prototype.navigateBack = function () {
        /*
         * If -n < x < 0, then the result of x % n will be x, which is
         * negative. To get a positive remainder, compute (x + n) % n.
         */
        var new_index = (this.cur_idx + this.visibleTargets.length - 1) % this.visibleTargets.length;

        this.track_event('PG.back');
        this.show_message(new_index, true);
        return false;
    };

    /**
     * navigate to the next step. if at last step, loop back to the first.
     **/
    tl.pg.PageGuide.prototype.navigateForward = function () {
        var new_index = (this.cur_idx + 1) % this.visibleTargets.length;

        this.track_event('PG.fwd');
        this.show_message(new_index, true);
        return false;
    };

    /**
     * open the pageguide! can be fired at any time, though it's usually done via
     * the toggle element (either boilerplate or user-specified) or the welcome
     * modal.
     **/
    tl.pg.PageGuide.prototype.open = function() {
        if (this.is_open) {
            return;
        } else {
            tl.pg.closeOpenGuides(this.uuid);
            this._open();
        }
    };

    tl.pg.PageGuide.prototype._open = function () {
        if (this.preferences.show_welcome) {
            this.preferences.dismiss_welcome();
            this.close_welcome();
        }
        this.is_open = true;
        this.track_event('PG.open');

        this.refreshVisibleSteps();

        if (this.preferences.auto_show_first && this.visibleTargets.length) {
            this.show_message(0);
        }
        $('body').addClass('tlypageguide-open');
        this.$toggle.addClass('tlyPageGuideToggleActive');

        var self = this;
        if (self.preferences.auto_refresh) {
            self.updateTimer(function () {
                self.updateVisible();
            }, 'overlay');
        }
    };

    tl.pg.PageGuide.prototype.updateTimer = function (cb, prop) {
        var self = this;
        cb();
        self.timer[prop] = setTimeout(function () {
            self.updateTimer(cb, prop);
        }, self.preferences.refresh_interval);
    };

    /**
     * close the pageguide. can also be fired at any time, though usually done via
     * the toggle or the close button.
     **/
    tl.pg.PageGuide.prototype.close = function() {
        if (!this.is_open) {
            return;
        } else {
            this._close();
        }
    };

    tl.pg.PageGuide.prototype._close = function () {
        this.is_open = false;
        this.track_event('PG.close');
        if (this.preferences.auto_refresh) {
            clearTimeout(this.timer.overlay);
        }

        this.$content.find('.tlypageguide_shadow').css('display', 'none');
        this.$content.find('.tlypageguide-active').removeClass('tlypageguide-active');
        this.$message.animate({ height: "0" }, 500, function() {
            $(this).hide();
        });

        $('body').removeClass('tlypageguide-open');
        this.$toggle.removeClass('tlyPageGuideToggleActive');
    };

    /**
     * bind all relevant event handlers within the document.
     **/
    tl.pg.PageGuide.prototype.setup_handlers = function () {
        var self = this;

        /* interaction: open/close PG interface */
        var interactor = (self.custom_open_button == null) ?
                        self.$base.find('#tlyPageGuideToggle' + self.uuid) :
                        $(self.custom_open_button);
        interactor.off();
        interactor.on('click', function() {
            if (self.is_open) {
                self.close();
            } else if (self.preferences.show_welcome &&
                      !self.preferences.check_welcome_dismissed() &&
                      !$('body').hasClass('tlyPageGuideWelcomeOpen')) {
                self.pop_welcome();
            } else {
                self.open();
            }
        });

        /* close guide */
        $('.tlypageguide_close', self.$message.add($('.tlypageguide_toggle')))
            .on('click', function() {
                self.close();
                return false;
        });

        /* interaction: item click */
        self.$base.on('click', '.tlyPageGuideStepIndex', function () {
            var selector = self.hashTable[$(this).parent().data('selectorhash')];
            var target = self.targetData[selector];
            var index = (target) ? target.index : 1;
            self.track_event('PG.specific_elt');
            self.show_message(index - 1);
        });

        /* interaction: fwd/back click */
        self.$fwd.on('click', function() {
            if (self.is_open) {
                self.navigateForward();
            }
            return false;
        });

        self.$back.on('click', function() {
            if (self.is_open) {
                self.navigateBack();
            }
            return false;
        });

        // pass through click events on overlays if necessary
        if (self.preferences.pointer_fallback) {
            self.$base.on('click', '.tlypageguide_shadow', function (e) {
                $(this).hide();
                var $bottomElement = $(document.elementFromPoint(e.clientX, e.clientY));
                if ($bottomElement.is('a')) {
                    $bottomElement.get(0).click(); // required for anchor click
                } else {
                    $bottomElement.trigger(e.type);
                }
                $(this).show();
            });
        }

        /* register resize callback */
        $(window).resize(function() {
            if (self.is_open) {
                self.refreshVisibleSteps();
            }
        });
    };

    /**
     * animate a given number to roll to the side.
     * num_wrapper (jQuery element): the element whose number to roll
     * new_text (string): the new text to roll across the element
     * left (boolean): whether or not to roll to the left-hand side
     **/
    tl.pg.PageGuide.prototype.roll_number = function (num_wrapper, new_text, left) {
        num_wrapper.animate({ 'text-indent': (left ? '' : '-') + '50px' }, 'fast', function() {
            num_wrapper.html(new_text);
            num_wrapper.css({ 'text-indent': (left ? '-' : '') + '50px' }, 'fast').animate({ 'text-indent': "0" }, 'fast');
        });
    };

    /**
     * pop up the welcome modal.
     **/
    tl.pg.PageGuide.prototype.pop_welcome = function () {
        $('body').addClass('tlyPageGuideWelcomeOpen');
        this.track_event('PG.welcomeShown');
    };

    /**
     * close the welcome modal.
     **/
    tl.pg.PageGuide.prototype.close_welcome = function () {
        $('body').removeClass('tlyPageGuideWelcomeOpen');
    };
}(jQuery));

<MSG> change reserved word char to c; close #40

<DFF> @@ -249,13 +249,13 @@ tl.pg.interval = {};
      * str (string): the string to be hashed
      **/
     tl.pg.hashCode = function (str) {
-        var hash = 0, i, char;
+        var hash = 0, i, c;
         if (str == null || str.length === 0) {
             return hash;
         }
         for (i = 0; i < str.length; i++) {
-            char = str.charCodeAt(i);
-            hash = ((hash<<5)-hash)+char;
+            c = str.charCodeAt(i);
+            hash = ((hash<<5)-hash)+c;
             hash = hash & hash;
         }
         return hash.toString();

change reserved word char to c; close #40