﻿/* 
AjaxDetailTip
---------
Description: 
A detail tip class that uses positiontip and iFrameShimFix for extension/composites.
This class is forked from DetailTip Class v0.4 for ajax functionality and needs merged.
created: 			6/2008 timw
last modified: 	9/23/2008 timw
version:				0.2
*/
var AjaxDetailTip = Class.create({
    // elTip : the id of the tip container (will be created if it doesn't exist in dom)
    // elTriggers : an array of elements that trigger showing a detail tip (must have rel="dt_#").
    initialize: function(elTip, elTriggers, options) {
        this.options = Object.extend({
            delay: 0.4, 							// Delay before tip is shown or hidden
            useEffects: false, 				// use Fade/Appear effects?
            useClose: true, 					// add a close link?
            triggerEvent: 'mouseover', 		// which event to trigger the showing of a tip? ('mouseover' or 'click')
            useMouseMove: false, 				// adjust the position on mouse move?
            useAjax: true, 						// wether or not the content comes through xhr
            xhrUrl: '/calendar/popover.aspx', // xhr url to retrieve content
            paramKeyName: 'performanceNumber'// querystring label for passing id's to xhr page
        }, options || {});

        // extend nested position preferences.
        this.positionPrefs = Object.extend({
            orientation: 'east',
            xOffset: 10,
            yOffset: 10,
            minMargin: 0,
            useCentering: true,
            useArrow: true,
            arrowClassName: 'dt_arrow',
            arrowDimensions: { width: 11, height: 11 }
        }, this.options.positionPrefs || {});

        this.elTip = $(elTip) || false;

        this.elTriggers = elTriggers;

        // element check
        if (this.elTriggers.length == 0) { return; }

        if (!this.elTip) {
            // tip doesn't exist, create it as first child of body
            this.elTip = new Element('div', { id: 'detailtip' });
            var tipMarkup =
				((this.positionPrefs.useArrow) ? '<div class="dt_arrow"></div>' : '')
				+ '<div class="dt_head">'
				+ ((this.options.useClose) ? '<div class="dt_close"><a href="#close#">close</a></div>' : '')
				+ '</div>'
				+ '<div class="detailtip_content"><!-- content will go here --></div>'
				+ '<div class="dt_foot"></div>'
			;
            this.elTip.update(tipMarkup);
            // generic markup only includes div.detailtip_content
            //this.elTip.insert({bottom: new Element('div', {className: 'detailtip_content'})});
            $(document.body).insert({ top: this.elTip });
        }
        else if (this.elTip.parentNode.nodeName != 'BODY') {
            // make sure tooltip is first child of body to avoid z-index issues
            $(document.body).insert({ top: this.elTip });
        }
        if (!this.elTip.down('div.detailtip_content')) {
            // make sure we have a content element
            this.elTip.insert({ bottom: new Element('div', { className: 'detailtip_content' }) });
        }

        this.elTipContent = this.elTip.down('div.detailtip_content');
        this.elTip.hide();
        this.isVisible = false;

        // attach event handlers
        this.elTip.observe('mouseover', this.__tipOver.bindAsEventListener(this));
        this.elTip.observe('mouseout', this.__tipOut.bindAsEventListener(this));

        var handle = this.__handleTriggerEvents.bindAsEventListener(this);
        this.elTriggers.invoke('observe', this.options.triggerEvent, handle);

        if (this.options.useClose) {
            var lnkClose = this.elTip.down('div.dt_close a');
            lnkClose.observe('click', this.__closeClick.bindAsEventListener(this));
            // also listen to mouseout for auto-closing when another trigger is moused over.
            this.elTriggers.invoke('observe', 'mouseout', handle);
        }
        else {
            this.elTriggers.invoke('observe', 'mouseout', handle);
        }

        if (this.options.useMouseMove) {
            this.elTriggers.invoke('observe', 'mousemove', handle);
        }
        // body click for user clicking outside tip to close tip
        Event.observe(document.body, 'click', this.__bodyClick.bindAsEventListener(this));

        // private members
        this._tpm = new TipPositionManager(this.positionPrefs); // class instance for managing tip positioning.
        this._isIE6 = false/*@cc_on || @_jscript_version < 5.7@*/;
        this._inTip = false;
        this._inTrigger = false;
        this._timeoutId = null;
        this._currentDetailKey = -1;
        this._effect = null;
        this._contentCache = $H();
        this._lastTriggerIndex = null;
        this._keyInTransit = false;
        this._approxXhrLatencyMS = 200; //should be no more than this.options.delay
    },

    __bodyClick: function(e) {
        // note:e.relatedTarget is the node to which the pointer went, e.currentTarget is the node to which the event is attached
        var el = e.element();
        //may need to also make sure el is not in a trigger (this.elTriggers) before hiding.
        if (this.isVisible && !el.descendantOf(this.elTip)) {
            this.hideTip();
        }
    },

    __tipOver: function(e) {
        this._inTip = true;
    },

    __tipOut: function(e) {
        if (e.relatedTarget && !e.relatedTarget.descendantOf(this.elTip)) {
            this._inTip = false
        }
        if (!this.options.useClose) {
            // auto-close
            this._clearTimer();
            this._timeoutId = setTimeout(this.hideTip.bind(this), this.options.delay * 1000);
        }
    },

    __closeClick: function(e) {
        e.stop();
        this.hideTip(true);
    },

    __handleTriggerEvents: function(e) {
        var trigger = e.element();
        // console.log(e.type);
        // make sure we have the trigger, not a child node.
        while (this.elTriggers.indexOf(trigger) == -1 && trigger.nodeName != 'BODY') {
            trigger = trigger.up();
        }
        var triggerIndex = this.elTriggers.indexOf(trigger);

        // mouseover or click
        /////////////////////
        if (e.type == this.options.triggerEvent) {
            this._inTrigger = true;
            var isNewTrigger = this._lastTriggerIndex !== triggerIndex;

            var key = this.getTipKey(trigger); // key == tipID
            if (!key || this._inTip) { console.log('invalid key in trigger'); return; } // invalid key or user is in the tip

            var isNewKey = (this._currentDetailKey != key); // requesting a different key id (could already exist)

            //console.log('this.isVisible: ' + this.isVisible + ', isNewKey: ' + isNewKey + ', isNewTrigger: ' + isNewTrigger + ', this._keyInTransit: ' + this._keyInTransit + ', this._timeoutId: ' + this._timeoutId);

            if (isNewTrigger) {
                this._lastTriggerIndex = triggerIndex;
                this._currentDetailKey = key;
                this._tpm.setPosition(trigger, this.elTip, { left: e.clientX, top: e.clientY });
                if (this._isIE6) {
                    this._tpm.iFrameShimFix('activate', this.elTip);
                }
            }

            if (isNewKey && this.isVisible) {
                //console.log('forcing hide of previous detail');
                if (this.options.useEffects && this._effect) {
                    this._effect.cancel();
                }
                this.hideTip(true);
            }

            this._clearTimer();

            if (isNewKey) {
                // we have a new id that isn't in the cache and we want to use ajax
                if (!this._contentCache.get(key) && this.options.useAjax) {
                    this._timeoutId = setTimeout(this.xhrRequestContent.bind(this, key), ((this.options.delay * 1000) - this._approxXhrLatencyMS));
                }
                else {
                    // non-ajax
                    var content = this.getLocalContent(key);
                    this.updateContent(content);
                    this._timeoutId = setTimeout(this.showTip.bind(this, key), this.options.delay * 1000);
                }
            }
            else {
                if (this.options.useAjax && key !== this._keyInTransit) {
                    if (!this._contentCache.get(key)) {
                        // deal with trigger hover, then off, then back on again before content was requested
                        //console.log('not a new key, not in transit, not in cache, go get it');
                        this._timeoutId = setTimeout(this.xhrRequestContent.bind(this, key), ((this.options.delay * 1000) - this._approxXhrLatencyMS));
                    }
                    else {
                        //console.log('not a new key, not in transit, but exists in cache--put showTip on timer');					
                        this._timeoutId = setTimeout(this.showTip.bind(this, key), this.options.delay * 1000);
                    }
                }
                else {
                    // non-ajax old key (TODO: need to double-check this logic for non-ajax)
                    this._timeoutId = setTimeout(this.showTip.bind(this, key), this.options.delay * 1000);
                }
            }

            //removed from here

        }
        // mousemove
        /////////////////////
        else if (e.type == 'mousemove') {
            // reposition the tip according to mouse movement
            this._tpm.setPosition(trigger, this.elTip, { left: e.clientX, top: e.clientY });
            if (this._isIE6) {
                this._tpm.iFrameShimFix('activate', this.elTip);
            }
        }
        // mouseout
        /////////////////////
        else if (e.type == 'mouseout') {
            this._inTrigger = false;

            this._clearTimer();
            if (!this.options.useClose) {
                this._timeoutId = setTimeout(this.hideTip.bind(this), this.options.delay * 1000);
            }
        }
    },

    getTipKey: function(trigger) {
        return trigger.readAttribute('rel') || null;
    },

    getLocalContent: function(tipID) {
        //console.log('getting local content for ' + tipID);
        if (!this._contentCache.get(tipID)) {
            //add to cache
            this._contentCache.set(tipID, $(tipID).innerHTML);
        }
        return this._contentCache.get(tipID);
    },

    xhrRequestContent: function(tipID) {
        //console.log('getting remote content for ' + tipID);
        var recordID = /\d+\w*$/.exec(tipID); // parse the performance id from tipID (e.g. dt_629)
        new Ajax.Request(this.options.xhrUrl, {
            method: 'get',
            parameters: this.options.paramKeyName + '=' + recordID,
            onSuccess: this.__xhrSuccess.bind(this),
            onFailure: this.__xhrFailure.bind(this)
        });
        this._keyInTransit = tipID;
    },

    __xhrSuccess: function(transport) {
        // save response
        var form = transport.responseXML.getElementsByTagName('form')[0];
        // get the id for this response
        var recordID = transport.responseXML.getElementsByTagName('perfno')[0].firstChild.nodeValue;
        var tipID = 'dt_' + recordID; // markup to following convention rel="dt_#"
        this._keyInTransit = false;
        this._clearTimer();

        for (var i = 0; i < form.childNodes.length; i++) {
            var node = form.childNodes[i];
            if (node.nodeType == 4) {// 4 = CDATA section
                // save response in dom cache.
                this._contentCache.set(tipID, node.nodeValue);
                // make sure the user hasn't moved on.
                if (this._currentDetailKey == tipID) {
                    this.updateContent(node.nodeValue);
                    this.showTip(tipID);
                }

                break;
            }
        }
    },

    __xhrFailure: function(transport) {
        this._keyInTransit = false;
        console.log('something went wrong with the xhr request.\n"' + transport.status + ':' + transport.statusText + '"\ntransport:');
        console.log(transport);
        this.hideTip();
    },

    updateContent: function(content) {
        this.elTipContent.update(content);
    },

    showTip: function(tipID) {
        //console.log('show failed for ' + tipID);
        if (this._inTrigger && (tipID == this._currentDetailKey)) {
            //console.log('show passed for ' + tipID);
            if (this.options.useEffects) {
                this._effect = new Effect.Appear(this.elTip, { duration: 0.15 });
            }
            else {
                this.elTip.show();
            }

            this.isVisible = true;
        }
    },

    hideTip: function(force) {
        //console.log('hide fired');
        if (!this._inTip || (force === true)) {
            if (this.options.useEffects && force != true) {
                this._effect = new Effect.Fade(this.elTip, { duration: 0.15 });
            }
            else {
                this.elTip.hide();
            }

            this.isVisible = false;

            if (this._isIE6) {
                this._tpm.iFrameShimFix('deactivate', this.elTip);
            }
        }
    },

    _clearTimer: function() {
        if (typeof (this._timeoutId) == 'number') {
            clearTimeout(this._timeoutId);
        }
        this._timeoutId == null;
    }
});
