Ext.namespace('Ait.Aspect');

Ait.Aspect.HomePanel = {
    _panel: null,

    //
    // Initialize the home panel
    //
    init: function() {
        this._panel = new Ext.Panel({
            region: 'center',
            contentEl: 'homePanel',
            margins: '4 3 0 0',
            autoScroll: true,
            containerScroll: true
        });  
        
        this._panel.setVisible(true);
    },
    
    //
    // Get panel
    //
    getPanel: function() {
        return this._panel;
    }
};
Ext.namespace('Ait.Aspect.Services');

//
// Access to web services.
//
// TODO: Ait.web will eventually be responsible for generating
//       these proxies to the web services.  This file will be removed
//       when that work is done.
Ext.lib.Ajax.setDefaultPostHeader(false);

// Set application base url here (must end in '/')
//Ait.Aspect.Services.BASE_URL = ''; this is now set in Default.aspx

Ait.Aspect.Services.Page = {
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL +'Services/Page.asmx',

    get: function(pageId, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Get', {pageId: pageId}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.Search = { 
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/Search.asmx',
  
    searchContent: function(searchString, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'SearchContent', {searchString: searchString}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.Toc = {
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/Toc.asmx',

    getChildren: function(parentId, parentEntryType, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'GetChildren', {parentId: parentId, parentEntryType: parentEntryType}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    },
    
    getNestedToNodeId: function(nodeId, entryType, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'GetNestedToNodeId', {nodeId: nodeId, entryType: entryType}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    },
    
    libraryCodeToTocEntryID: function(libraryBookId, libraryTopicId, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'LibraryCodeToTocEntryID', {libraryBookId: libraryBookId, libraryTopicId: libraryTopicId}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.Favorite = {
    _serviceUrl: Ait.Aspect.Services.ENGINEWEB2_BASE_URL + 'Services/Favorite.asmx',

    add: function(reference, title, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Add', {reference: reference, title: title}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    },
    
    get: function(callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Get', null, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    },
    
    remove: function(favoriteId, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Remove', {favoriteId: favoriteId}, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.Site = {
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/Site.asmx',

    get: function(callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Get', null, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.FilterCriteria = {
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/FilterCriteria.asmx',

    get: function(callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Get', null, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.User = {
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/User.asmx',

    updateProperties: function(userProperties, ignoreReadOnly, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'UpdateProperties', { userProperties: userProperties, ignoreReadOnly: ignoreReadOnly }, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.Aspect.Services.Util = {
    _serviceUrl: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/Util.asmx'

};

Ait.Aspect.Services.Session = {
    _serviceUrl: Ait.Aspect.Services.ENGINEWEB2_BASE_URL + 'Services/Session.asmx',

    poll: function(callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        Ait.ServiceHelper.callServiceMethod(this._serviceUrl, 'Poll', null, callback, callbackScope, exceptionCallback, exceptionCallbackScope);
    }
};

Ait.ServiceHelper = {
    //
    // Call method on json webservice
    //
    callServiceMethod: function(serviceUrl, methodName, params, callback, callbackScope, exceptionCallback, exceptionCallbackScope) {
        // Process params into json    
        Ext.Ajax.request({
            headers: {'Content-Type': 'application/json'},
            method: 'POST',
            url: serviceUrl + "/" + methodName,
            success: this._callServiceMethod_response,
            scope: this,
            params: Ext.encode(params),
            paramsObj: params,
            argument: {callback: callback, callbackScope: callbackScope, exceptionCallback: exceptionCallback, exceptionCallbackScope: exceptionCallbackScope}
        });
    },
    
    //
    // Response received from service method
    //
    _callServiceMethod_response: function(response, options) {
        var responseObj = eval('(' + response.responseText + ')');
        
        if(responseObj.IsException === true) {
            if(options.argument.exceptionCallback != null) {
                if(!options.argument.exceptionCallbackScope)
                    options.argument.exceptionCallbackScope = this;
            
                options.argument.exceptionCallback.call(options.argument.exceptionCallbackScope, responseObj.Exception.Message, options.paramsObj);
            }
        }
        else {
            if(options.argument.callback != null) {
                if(!options.argument.callbackScope)
                    options.argument.callbackScope = this;
            
                options.argument.callback.call(options.argument.callbackScope, responseObj.Value, options.paramsObj);
            }
        }
    }
};
Ext.namespace('Ait.Aspect');

//
// Toc entry type.
//
Ait.Aspect.TocEntryType = {
    Site: 1,
    Book: 2
}

//
// Import job status.
//
Ait.Aspect.ImportJobStatus = {
    New: 0,
    Importing: 1,
    Success: 2,
    Failure: 3
}

//
// Import job log entry type
//
Ait.Aspect.ImportJobLogEntryType = {
    Info: 1,
    Warning: 2,
    Progress: 3,
    Failure: 4,
    Success: 5  
}

//
// Variable Type
//
Ait.Aspect.VariableType = {
    Normal: 0,
    ListOfValues: 1
}

//
// Variable Type
//
Ait.Aspect.VariableType = {
    Normal: 0,
    ListOfValues: 1
}

//
// Service connection
//
Ait.Aspect.ServiceConnectionStatus = {
    Disconnected: 1,
    Connected: 2
}

//
// Email ids
//
Ait.Aspect.Emails = {
    FeedbackEmail: 1,
    PageEmail: 2
}
Ext.namespace('Ait.Aspect');

//
// Escape literal string for use in regexp
//
RegExp.escape = function(text) {
  if (!arguments.callee.sRE) {
    var specials = [
      '/', '.', '*', '+', '?', '|',
      '(', ')', '[', ']', '{', '}', '\\'
    ];
    arguments.callee.sRE = new RegExp(
      '(\\' + specials.join('|\\') + ')', 'g'
    );
  }
  return text.replace(arguments.callee.sRE, '\\$1');
}

//
// Detect FF3 - borrowed from Ext 3
//
Ext.isGecko3 = Ext.isGecko && /rv:1\.9/.test(navigator.userAgent.toLowerCase());

Ait.Aspect.DomUtil = {
    //
    // Scroll a container to the top of an element.
    // TODO: Extend the Ext.Element class to include this function, rather
    // than deliver it in a separate util singleton.
    //
    scrollToTop: function(elem, container) {
        var p = Ext.get(container);
        var d = Ext.get(elem);
        var yOffset = d.getY() - (p.getY() - p.dom.scrollTop);
        p.scrollTo("top", yOffset, true);   
    }
};

//
// Email utility
//
Ait.Aspect.MailUtil = {

    //
    // Make a mailto link and open it.
    //
    mailto: function(email, subject, body) {
        var mailtoLink = 'mailto:' + escape(email) + '?subject=' + escape(subject) + '&body=' + escape(body);
        window.open(mailtoLink);
    }  
};


/**
 * Makes a ComboBox more closely mimic an HTML SELECT.  Supports clicking and dragging
 * through the list, with item selection occurring when the mouse button is released.
 * When used will automatically set {@link #editable} to false and call {@link Ext.Element#unselectable}
 * on inner elements.  Re-enabling editable after calling this will NOT work.
 *
 * @author Corey Gilmore
 * http://extjs.com/forum/showthread.php?t=6392
 *
 * @history 2007-07-08 jvs
 * Slight mods for Ext 2.0
 */
Ext.ux.SelectBox = function(config){
	this.searchResetDelay = 1000;
	config = config || {};
	config = Ext.apply(config || {}, {
		editable: false,
		forceSelection: true,
		rowHeight: false,
		lastSearchTerm: false,
        triggerAction: 'all',
        mode: 'local'
    });

	Ext.ux.SelectBox.superclass.constructor.apply(this, arguments);

	this.lastSelectedIndex = this.selectedIndex || 0;
};

Ext.extend(Ext.ux.SelectBox, Ext.form.ComboBox, {
    lazyInit: false,
	initEvents : function(){
		Ext.ux.SelectBox.superclass.initEvents.apply(this, arguments);
		// you need to use keypress to capture upper/lower case and shift+key, but it doesn't work in IE
		this.el.on('keydown', this.keySearch, this, true);
		this.cshTask = new Ext.util.DelayedTask(this.clearSearchHistory, this);
	},

	keySearch : function(e, target, options) {
		var raw = e.getKey();
		var key = String.fromCharCode(raw);
		var startIndex = 0;

		if( !this.store.getCount() ) {
			return;
		}

		switch(raw) {
			case Ext.EventObject.HOME:
				e.stopEvent();
				this.selectFirst();
				return;

			case Ext.EventObject.END:
				e.stopEvent();
				this.selectLast();
				return;

			case Ext.EventObject.PAGEDOWN:
				this.selectNextPage();
				e.stopEvent();
				return;

			case Ext.EventObject.PAGEUP:
				this.selectPrevPage();
				e.stopEvent();
				return;
		}

		// skip special keys other than the shift key
		if( (e.hasModifier() && !e.shiftKey) || e.isNavKeyPress() || e.isSpecialKey() ) {
			return;
		}
		if( this.lastSearchTerm == key ) {
			startIndex = this.lastSelectedIndex;
		}
		this.search(this.displayField, key, startIndex);
		this.cshTask.delay(this.searchResetDelay);
	},

	onRender : function(ct, position) {
		this.store.on('load', this.calcRowsPerPage, this);
		Ext.ux.SelectBox.superclass.onRender.apply(this, arguments);
		if( this.mode == 'local' ) {
			this.calcRowsPerPage();
		}
	},

	onSelect : function(record, index, skipCollapse){
		if(this.fireEvent('beforeselect', this, record, index) !== false){
			this.setValue(record.data[this.valueField || this.displayField]);
			if( !skipCollapse ) {
				this.collapse();
			}
			this.lastSelectedIndex = index + 1;
			this.fireEvent('select', this, record, index);
		}
	},

	render : function(ct) {
		Ext.ux.SelectBox.superclass.render.apply(this, arguments);
		if( Ext.isSafari ) {
			this.el.swallowEvent('mousedown', true);
		}
		this.el.unselectable();
		this.innerList.unselectable();
		this.trigger.unselectable();
		this.innerList.on('mouseup', function(e, target, options) {
			if( target.id && target.id == this.innerList.id ) {
				return;
			}
			this.onViewClick();
		}, this);

		this.innerList.on('mouseover', function(e, target, options) {
			if( target.id && target.id == this.innerList.id ) {
				return;
			}
			this.lastSelectedIndex = this.view.getSelectedIndexes()[0] + 1;
			this.cshTask.delay(this.searchResetDelay);
		}, this);

		this.trigger.un('click', this.onTriggerClick, this);
		this.trigger.on('mousedown', function(e, target, options) {
			e.preventDefault();
			this.onTriggerClick();
		}, this);

		this.on('collapse', function(e, target, options) {
			Ext.getDoc().un('mouseup', this.collapseIf, this);
		}, this, true);

		this.on('expand', function(e, target, options) {
			Ext.getDoc().on('mouseup', this.collapseIf, this);
		}, this, true);
	},

	clearSearchHistory : function() {
		this.lastSelectedIndex = 0;
		this.lastSearchTerm = false;
	},

	selectFirst : function() {
		this.focusAndSelect(this.store.data.first());
	},

	selectLast : function() {
		this.focusAndSelect(this.store.data.last());
	},

	selectPrevPage : function() {
		if( !this.rowHeight ) {
			return;
		}
		var index = Math.max(this.selectedIndex-this.rowsPerPage, 0);
		this.focusAndSelect(this.store.getAt(index));
	},

	selectNextPage : function() {
		if( !this.rowHeight ) {
			return;
		}
		var index = Math.min(this.selectedIndex+this.rowsPerPage, this.store.getCount() - 1);
		this.focusAndSelect(this.store.getAt(index));
	},

	search : function(field, value, startIndex) {
		field = field || this.displayField;
		this.lastSearchTerm = value;
		var index = this.store.find.apply(this.store, arguments);
		if( index !== -1 ) {
			this.focusAndSelect(index);
		}
	},

	focusAndSelect : function(record) {
		var index = typeof record === 'number' ? record : this.store.indexOf(record);
		this.select(index, this.isExpanded());
		this.onSelect(this.store.getAt(record), index, this.isExpanded());
	},

	calcRowsPerPage : function() {
		if( this.store.getCount() ) {
			this.rowHeight = Ext.fly(this.view.getNode(0)).getHeight();
			this.rowsPerPage = this.maxHeight / this.rowHeight;
		} else {
			this.rowHeight = false;
		}
	}

});


//
// Column tree
//
Ext.tree.ColumnTree = Ext.extend(Ext.tree.TreePanel, {
    lines:false,
    borderWidth: Ext.isBorderBox ? 0 : 2, // the combined left/right border for each cell
    cls:'x-column-tree',
    
    onRender : function(){
        Ext.tree.ColumnTree.superclass.onRender.apply(this, arguments);
        this.headers = this.body.createChild(
            {cls:'x-tree-headers'},this.innerCt.dom);

        var cols = this.columns, c;
        var totalWidth = 0;

        for(var i = 0, len = cols.length; i < len; i++){
             c = cols[i];
             totalWidth += c.width;
             this.headers.createChild({
                 cls:'x-tree-hd ' + (c.cls?c.cls+'-hd':''),
                 cn: {
                     cls:'x-tree-hd-text',
                     html: c.header
                 },
                 style:'width:'+(c.width-this.borderWidth)+'px;'
             });
        }
        this.headers.createChild({cls:'x-clear'});
        // prevent floats from wrapping when clipped
        this.headers.setWidth(totalWidth);
        this.innerCt.setWidth(totalWidth);
    }
});

Ext.tree.ColumnNodeUI = Ext.extend(Ext.tree.TreeNodeUI, {
    focus: Ext.emptyFn, // prevent odd scrolling behavior

    renderElements : function(n, a, targetNode, bulkRender){
        this.indentMarkup = n.parentNode ? n.parentNode.ui.getChildIndent() : '';

        var t = n.getOwnerTree();
        var cols = t.columns;
        var bw = t.borderWidth;
        var c = cols[0];

        var buf = [
             '<li class="x-tree-node"><div ext:tree-node-id="',n.id,'" class="x-tree-node-el x-tree-node-leaf ', a.cls,'">',
                '<div class="x-tree-col x-tree-col-first" style="width:',c.width-bw,'px;">',
                    '<span class="x-tree-node-indent">',this.indentMarkup,"</span>",
                    '<img src="', this.emptyIcon, '" class="x-tree-ec-icon x-tree-elbow">',
                    '<img src="', a.icon || this.emptyIcon, '" class="x-tree-node-icon',(a.icon ? " x-tree-node-inline-icon" : ""),(a.iconCls ? " "+a.iconCls : ""),'" unselectable="on">',
                    '<a hidefocus="on" class="x-tree-node-anchor" href="',a.href ? a.href : "#",'" tabIndex="1" ',
                    a.hrefTarget ? ' target="'+a.hrefTarget+'"' : "", '>',
                    '<span unselectable="on">', n.text || (c.renderer ? c.renderer(a[c.dataIndex], n, a) : a[c.dataIndex]),"</span></a>",
                "</div>"];
         for(var i = 1, len = cols.length; i < len; i++){
             c = cols[i];

             buf.push('<div class="x-tree-col x-tree-col-other ',(c.cls?c.cls:''),'" style="width:',c.width-bw,'px;">',
                        '<div class="x-tree-col-text">',(c.renderer ? c.renderer(a[c.dataIndex], n, a) : a[c.dataIndex]),"</div>",
                      "</div>");
         }
         buf.push(
            '<div class="x-clear"></div></div>',
            '<ul class="x-tree-node-ct" style="display:none;"></ul>',
            "</li>");

        if(bulkRender !== true && n.nextSibling && n.nextSibling.ui.getEl()){
            this.wrap = Ext.DomHelper.insertHtml("beforeBegin",
                                n.nextSibling.ui.getEl(), buf.join(""));
        }else{
            this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf.join(""));
        }

        this.elNode = this.wrap.childNodes[0];
        this.ctNode = this.wrap.childNodes[1];
        var cs = this.elNode.firstChild.childNodes;
        this.indentNode = cs[0];
        this.ecNode = cs[1];
        this.iconNode = cs[2];
        this.anchor = cs[3];
        this.textNode = cs[3].firstChild;
    }
});


//
// Patch allowBlank to trim value.
//
Ext.apply(Ext.form.TextField.prototype,{ 
    validator:function(text){
        if(this.allowBlank==false && Ext.util.Format.trim(text).length==0)
          return false;
        else
          return true;
    }
});

// Get Query String value From Url
function getQueryStringValueFromUrl(url, parameterName) {

    var href = url;
    
    var questionMarkPos = href.indexOf("?");
    if(questionMarkPos < 0) return "";

    var hashPos = href.indexOf("#");
    
    var query = href.substring(questionMarkPos + 1);
    if (hashPos >= 0)
        query = href.substring(questionMarkPos + 1, hashPos);
        
    var queryParameters = query.split("&");
    for (var i = 0; i < queryParameters.length; i++) {
        var param = queryParameters[i];
        var nvp = param.split("=");
        if (nvp[0] == parameterName)
            return unescape(nvp[1]);
    }
    return "";
};

// Remove Query String From Url
function removeQueryStringsFromUrl(url, parametersToRemove) {

    var questionMarkPos = url.indexOf("?");
    if (questionMarkPos < 0) return url;

    //get hash value
    var hashValue = ""; //includes the hash sign
    var hashPos = url.indexOf("#");
    if (hashPos >= 0) {
        hashValue = url.substr(hashPos);
        url = url.substr(0, hashPos);
    }

    //get query value (we already know questionMarkPos >= 0 from initial constraint)
    var query = url.substr(questionMarkPos);
    var baseUrl = url.substr(0, questionMarkPos);
    
    var queryComponents = [];
    var queryParameters = query.split("&");
    for (var i = 0, ci = queryParameters.Length; i < ci; i++) {
        var pair = queryParameters[i].split("=");
        var name = pair[0];

        //are we removing this name?
        for (var j = 0, cj = parametersToRemove.length; j < cj; j++)
            if (parametersToRemove == name)
                break;

        //parameter to remove was found, so do not add it.
        if (j < cj) continue;

        queryComponents.push(queryParameters[i]);
    }

    var newQueryString = (queryComponents.length <= 0) ? "" : "?" + queryComponents.join("&");
    return baseUrl + newQueryString + hashValue;
};

//
// Modified tool-tip that remains active while the mouse is over the tool-tip panel as well as the link.
// http://extjs.com/forum/showthread.php?p=192455#post192455
//
Ext.ux.PersistentTip = Ext.extend(Ext.ToolTip, {
    initComponent: function() {
        Ext.apply(this, {
            showDelay: 500,
            hideDelay: 500,
            autoHide: false
        });
        Ext.ux.PersistentTip.superclass.initComponent.call(this);
    },

    afterRender: function() {
        Ext.ux.PersistentTip.superclass.afterRender.call(this);
        this.el.on('mouseout', this.onTargetOut, this);
        this.el.on('mouseover', this.onElOver, this);
        this.target.on('click', this.hide, this);
    },

    checkWithin: function(e) {
        if (this.el && e.within(this.el.dom, true)) {
            return true;
        }
        if (this.disabled || e.within(this.target.dom, true)) {
            return true;
        }
        return false;
    },

    onElOver: function(e) {
        if (this.checkWithin(e)) {
            this.clearTimer('hide');
        }
    },

    onTargetOver: function(e) {
        if (this.disabled || e.within(this.target.dom, true)) {
            return;
        }
        this.clearTimer('hide');
        this.targetXY = e.getXY();
        this.delayShow(e);
    },

    delayShow: function(e) {
        this.showTimer = this.doShow.defer(this.showDelay, this, [e]);
    },

    doShow: function(e) {
        var xy = e.getXY();
        var within = this.target.getRegion().contains({ left: xy[0], right: xy[0], top: xy[1], bottom: xy[1] });
        if (within) {
            this.show();
        }
    },

    onTargetOut: function(e) {
        if (this.checkWithin(e)) {
            this.clearTimer('hide');
        } else if (this.hideTimer) {
            this.hide();
        } else {
            this.delayHide();
        }
    }
});

//
// Session Keep-Alive (Singleton)
//
//Keep-Alive uses the singleton with inheritance pattern:
//(from http://extjs.com/forum/showthread.php?t=6862&highlight=singleton)
//
Ait.Aspect.KeepAlive = function() {
    return Ext.apply(new Ext.util.Observable(), {

        _periodMillis: 5 * 60 * 1000, //5 minutes

        _keepAlive: function() {
            Ait.Aspect.Services.Session.poll();
            this._keepAlive.defer(this._periodMillis, this);
        },

        start: function() {
            this._keepAlive.defer(this._periodMillis, this);
        }
    });
} ();

//
// Gets the rule for a specific class from a loaded stylesheet.
//
Ait.Aspect.GetClassFromStylesheet = function(cssHref, cssClassSelector) {

    var styleSheets = document.styleSheets;
    for (var i = 0, ci = styleSheets.length; i < ci; i++) {

        var stylesheet = styleSheets[i];
        if (stylesheet.href != cssHref) continue;

        var rules = stylesheet.rules;
        for (var j = 0, cj = rules.length; j < cj; j++) {

            var rule = rules[j];
            if (rule.selectorText == cssClassSelector)
                return rule;
        }
    }
    return null;
}
Ext.namespace('Ait.Aspect');

//
// Table of Contents Tree
//  - Loader
//  - ID parser/builder
//


//
// Toc tree loader
// 
Ait.Aspect.TocTreeLoader = function(config){
    this.baseParams = {};
    Ext.apply(this, config);
    Ait.Aspect.TocTreeLoader.superclass.constructor.call(this);
};

Ext.extend(Ait.Aspect.TocTreeLoader, Ext.tree.TreeLoader, {

    requestData: function(node, callback) {
        if (this.fireEvent("beforeload", this, node, callback) === false) {
            if (typeof callback == "function") callback();
            return;
        }

        var idParts = Ait.Aspect.TocNodeId.parse(node.id);

        this.transId = Ext.Ajax.request({
            headers: { 'Content-Type': 'application/json' },
            method: 'POST',
            url: (this.dataUrl || this.url),
            success: this.handleResponse,
            failure: this.handleFailure,
            scope: this,
            argument: { callback: callback, node: node },
            params: Ext.encode({ parentId: idParts.id, parentEntryType: idParts.type })
        });
    },

    processResponse: function(response, node, callback) {
        // Insert data from format returned by Toc.asmx
        var json = response.responseText;
        try {
            var o = eval("(" + json + ")");


            if (this.loadRecursive === true) {
                this.loadWithChildren(node, o.Value);
            }
            else {
                node.beginUpdate();
                var items = o.Value;
                for (var i = 0, len = items.length; i < len; i++) {
                    var n = this.createNode(this._getNodeData(items[i]));
                    if (n) {
                        node.appendChild(n);
                    }
                }
                node.endUpdate();
            }

            if (typeof callback == "function") {
                callback(this, node);
            }
        } catch (e) {
            this.handleFailure(response);
        }
    },

    //
    // Transform node data from json returned to json ext tree supports
    //
    _getNodeData: function(item) {
        var nodeData = {
            id: Ait.Aspect.TocNodeId.get(item.Id, item.EntryType),
            text: item.Text,
            iconCls: item.IsLeaf ? 'topicIcon' : '',
            leaf: this.forceBranch == true ? false : item.IsLeaf,
            uiProvider: this.defaultUIProvider || '',
            next: (item.NextId) ? Ait.Aspect.TocNodeId.get(item.NextId, item.NextType) : null,
            previous: (item.PreviousId) ? Ait.Aspect.TocNodeId.get(item.PreviousId, item.PreviousType) : null
        };

        if (item.EntryType == Ait.Aspect.TocEntryType.Book) {
            nodeData.pageId = item.PageId;
            nodeData.topicId = item.TopicId;
            nodeData.libraryTopicId = item.LibraryTopicId;
        }
        else if (item.EntryType == Ait.Aspect.TocEntryType.Site) {
            nodeData.reference = item.Reference;
            nodeData.bookTitle = item.BookTitle;
            nodeData.libraryBookId = item.LibraryBookId;
        }

        return nodeData;
    },

    //
    // Recursively load nodes, including children.
    //
    loadWithChildren: function(node, childNodeData) {

        node.beginUpdate();
        for (var i = 0, len = childNodeData.length; i < len; i++) {
            // Get node data in format needed for ext tree
            var transformedChildNodeData = this._getNodeData(childNodeData[i]);

            // Only create nodes that don't exist already in tree
            var currentNode = node.ownerTree.nodeHash[transformedChildNodeData.id];

            if (currentNode == null) {
                currentNode = this.createNode(transformedChildNodeData);
                if (currentNode) {
                    node.appendChild(currentNode);
                }
            }

            if (this.forceBranch && childNodeData[i].Children == null)
                childNodeData[i].Children = [];

            // Recursively add children
            if (childNodeData[i].Children != null) {
                this.loadWithChildren(currentNode, childNodeData[i].Children);
            }
        }
        node.loaded = true;
        node.endUpdate();
    }
});


//
// Toc Node id functions
//
Ait.Aspect.TocNodeId = {

    //
    // Make node id.
    //
    get: function(id, type) {
        if(type == Ait.Aspect.TocEntryType.Site)
            return "s" + id;
        else
            return "b" + id;
    },
    
    //
    // Parse toc node id
    //
    parse: function(nodeId) {    
        if(nodeId.charAt(0) == "s")
            return {id: nodeId.substr(1), type: Ait.Aspect.TocEntryType.Site};
        else if(nodeId.charAt(0) == "b")
            return {id: nodeId.substr(1), type: Ait.Aspect.TocEntryType.Book};
        else
            return null;
    },

    validPrefixes: ["s", "b"],
    IsValidPrefix: function(hash) {
        for (var i = 0; i < this.validPrefixes.length; i++)
            if (hash.indexOf(this.validPrefixes[i]) == 0)
            return true;
        return false;
    }
}
Ext.namespace('Ait.Web.JsonService');

//
// Json Service <=> Ext bridge.
// Compatible with Ext 2.02
// This should really be served up from the Ait.Web project.
//

//
// Json Service Data Proxy
//
Ait.Web.JsonService.Proxy = function(config) {
    this.baseParams = {};
    Ext.apply(this, config);
    Ait.Web.JsonService.Proxy.superclass.constructor.call(this);
        
    this.addEvents('beforeLoadResponse');
};

Ext.extend(Ait.Web.JsonService.Proxy, Ext.data.HttpProxy, {

    load: function(params, reader, callback, scope, arg) {      
        if(this.fireEvent("beforeload", this, params) !== false){
            var  o = {
                url: this.url,
                headers: {'Content-Type': 'application/json'},
                method: 'POST',
                params: Ext.encode(params),
                request: {
                    callback: callback,
                    scope: scope,
                    arg: arg
                },
                reader: reader,
                callback: this.loadResponse,
                scope: this
            };

            Ext.applyIf(o, this.conn);
            if(this.activeRequest){
                Ext.Ajax.abort(this.activeRequest);
            }
            this.activeRequest = Ext.Ajax.request(o);
                
        }else{
            callback.call(scope||this, null, arg, false);
        }
    },
    
    loadResponse : function(o, success, response){
        this.fireEvent("beforeLoadResponse", o, success, response);
        Ait.Web.JsonService.Proxy.superclass.loadResponse.call(this, o, success, response);
    }
});


//
// Json Data Store
//
Ait.Web.JsonStore = function(c){
    c.root = 'Value'; // Root is always Value

    Ait.Web.JsonStore.superclass.constructor.call(this, Ext.apply(c, {
        proxy: !c.data ? new Ait.Web.JsonServiceProxy({url: c.url}) : undefined,
        reader: new Ait.Web.JsonReader(c, c.fields)
    }));
};
Ext.extend(Ait.Web.JsonStore, Ext.data.JsonStore, {

    //
    // Load data
    //
    loadData: function(o, append) {
        Ait.Web.JsonStore.superclass.loadData.call(this, {Value: o}, append);
    }

});

//
// Json Grouping Store
//
Ait.Web.GroupingStore = function(c){
    c.root = 'Value'; // Root is always Value

    Ait.Web.GroupingStore.superclass.constructor.call(this, Ext.apply(c, {
        proxy: !c.data ? new Ait.Web.JsonServiceProxy({url: c.url}) : undefined,
        reader: new Ait.Web.JsonReader(c, c.fields)
    }));
};
Ext.extend(Ait.Web.GroupingStore, Ext.data.GroupingStore, {

    //
    // Load data
    //
    loadData: function(o, append) {
        Ait.Web.GroupingStore.superclass.loadData.call(this, {Value: o}, append);
    }
});


//
// Json Reader
//
Ait.Web.JsonReader = function(meta, recordType) {
    meta = meta || {};
    Ait.Web.JsonReader.superclass.constructor.call(this, meta, recordType || meta.fields);
};
Ext.extend(Ait.Web.JsonReader, Ext.data.JsonReader);
Ext.namespace("Ait.Aspect.EngineWeb2.Config");

Ait.Aspect.EngineWeb2.Config.TocAction = {
    Expanded: 0,    //Table-of-contents panel is displayed and can be closed by clicking (default).
    Collapsed: 1,    //Table-of-contents panel is not displayed and can be opened by clicking.
    Disabled :2    //Table-of-contents panel is not displayed and cannot be opened.
};

Ait.Aspect.EngineWeb2.Config.LinkAction = {
    Default: 0,     //No action taken (leave links behaviour as published)
    Disable: 1,     //links are rendered as plain text
    Internal: 2,   //links navigate to the same page/web part
    External: 3    //links allowed to navigate to external url
};

//
//
// Static class.  Call Decode to set the properties from an encoded value (e.g. from the querystring)
// 
// Usage:
//Ait.Aspect.EngineWeb2.Config.UI.Decode("AB--");
//x = Ait.Aspect.EngineWeb2.Config.UI.ShowToc;
Ait.Aspect.EngineWeb2.Config.UI = {

    EditMode: false,
    ShowHeader: true,
    ShowToolbar: true,
    ShowBreadCrumbs: true,
    ShowFooter: true,
    TocAction: Ait.Aspect.EngineWeb2.Config.TocAction.Expanded,
    LinkAction: Ait.Aspect.EngineWeb2.Config.LinkAction.Default,
    SelectedTheme: 0,
    ShowEmailPageLink: true,
    ShowSubmitFeedbackLink: true,

    // The Decode method will set the properties values according to the decoded value of 
    // Aspect Encoded configuration QueryString value
    Decode: function(encoded) {

        encoded = encoded.replace(/\./gi, '+').replace(/_/gi, '/').replace(/\-/gi, '='); //unescape our url-safe characters
        var unpacker = new Ait.Aspect.EngineWeb2.Config.BitUnpacker(encoded);

        this.EditMode = !!unpacker.getWord(1);
        this.ShowHeader = !!unpacker.getWord(1);// !! converts to boolean
        this.ShowToolbar = !!unpacker.getWord(1);
        this.ShowBreadCrumbs = !!unpacker.getWord(1);
        this.ShowFooter = !!unpacker.getWord(1);
        this.TocAction = unpacker.getWord(2);
        this.LinkAction = unpacker.getWord(2);
        this.SelectedTheme = unpacker.getWord(3);
        this.ShowEmailPageLink = !!unpacker.getWord(1);
        this.ShowSubmitFeedbackLink = !!unpacker.getWord(1);
    }
};

//
//
// This is a port of the BitUnpacker class from EngineWeb2.Config
//
Ait.Aspect.EngineWeb2.Config.BitUnpacker = function(base64encoded) {
    this.base64ToBits(base64encoded);
};

Ait.Aspect.EngineWeb2.Config.BitUnpacker.prototype = {

    _packed: [],    //bit array
    _bitIndex: 0,
    _base64Chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
    BITS_IN_BYTE: 8,

    base64ToBits: function(base64encoded) {
        this._packed = [];
        
        //Output starts at the most-significant-bit of the first byte
        var packedBitIndex = this.BITS_IN_BYTE; 

        for (var i = 0, ci = base64encoded.length; i < ci; i++) {

            var ch = base64encoded.charAt(i);
            if (ch == "=") continue;

            var value = this._base64Chars.indexOf(ch);
            if (value < 0 || value > 63)
                throw "Invalid character in base64 encoding";

            for (var j = 5; j >= 0; j--) { //6-bits per encoded charcter (hence range of 0-63 above)
                packedBitIndex--;
            
                var bit = value & (1 << j);
                if (bit) bit = 1;   //force bit to 0 or 1
                this._packed[packedBitIndex] = bit;

                //When we have set the least-significant-bit of the current byte,
                //move to the most-significant-bit of the next byte
                if (packedBitIndex % this.BITS_IN_BYTE == 0)
                    packedBitIndex += 2*this.BITS_IN_BYTE;
            }
        }

        //finish the current byte of output
        while (packedBitIndex % this.BITS_IN_BYTE != 0) {
            packedBitIndex--;
            this._packed[packedBitIndex] = 0;
        }
    },

    reset: function() {
        this._bitIndex = 0;
    },

    getWord: function(bitLength) {
        if (bitLength > 32) return 0;

        //combine requested number of bits into a number
        var output = 0;
        for (var i = 0; i < bitLength; i++, this._bitIndex++)
            output += this._packed[this._bitIndex] << i;

        return output;
    }
};
/* unFocus.History, version2.0 (Beta 2) (2007/09/10)
Copyright: 2005-2007, Kevin Newman (http://www.unfocus.com/Projects/HistoryKeeper/)
License: http://www.gnu.org/licenses/lgpl.html */
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('8 o={};o.Q=2(){h.j={};B(8 i=0;i<18.l;i++){h.j[18[i]]=[]}};o.Q.W={12:2(a,b){B(8 i=0;i<h.j[a].l;i++)4(h.j[a][i]==b)7;h.j[a].1Z(b)},1V:2(a,b){B(8 i=0;i<h.j[a].l;i++){4(h.j[a][i]==b){h.j.1S(i,1);7}}},p:2(a,b){B(8 i=0;i<h.j[a].l;i++)h.j[a][i](b)}};o.14=(2(){2 C(){8 c=h,E=1E,v,3;8 d=2(){7 1p.1k.23(1)};3=d();8 e=2(a){z.1p.1k=a};2 1f(){8 a=d();4(3!=a){3=a;c.p("n",a)}}4(O)v=O(1f,E);2 r(a){4(!1c(a)){8 b;4(/1b/.19(A.17)&&!z.16)b=6.w(\'<a G="\'+a+\'">\'+a+"</a>");u b=6.w("a");b.t("G",a);Z(b.D){V="U";1D="1A";1d=s()+"R";1v=1t()+"R"}6.k.L(b,6.k.P)}}2 1c(a){4(6.24(a).l>0)7 q}4(1i 1h.1g=="22"){2 s(){7 1h.1g}}u 4(6.N&&6.N.M){2 s(){7 6.N.M}}u 4(6.k){2 s(){7 6.k.M}}21(20(s).1X().1e(/1W/g,"1U").1e(/Y/g,"X"));c.1T=2(){7 3};2 9(a){4(3!=a){r(a);3=a;e(a);c.p("n",a)}7 q}c.9=2(a){r(3);c.9=9;7 c.9(a)};4(/1a\\/\\d+/.19(A.1n)&&A.1n.1o(/1a\\/(\\d+)/)[1]<1R){8 f=H.l,x={},m,y=15;2 S(){m=6.w("1O");m.13="1N";m.1M="1L";6.k.L(m,6.k.P)}e=2(a){x[f]=a;m.1K="#"+d();m.1J()};d=2(){7 x[f]};x[f]=3;2 T(a){4(3!=a){r(a);3=a;f=H.l+1;y=q;e(a);c.p("n",a);y=15}7 q}c.9=2(a){r(3);S();c.9=T;7 c.9(a)};2 10(){4(!y){8 a=H.l;4(a!=f){f=a;8 b=d();4(3!=b){3=b;c.p("n",b)}}}};1I(v);v=O(10,E)}u 4(1i 1H!="1G"&&z.1F&&!z.16&&A.17.1o(/1b (\\d\\.\\d)/)[1]>=5.5){8 g,F;2 11(){8 a="1C";g=6.w("1B");g.t("G",a);g.t("13",a);g.t("1P",\'1Q:;\');g.D.V="U";g.D.1d="-1z";6.k.L(g,6.k.P);F=1y[a];J(3,q)}2 J(a){Z(F.6){1x("1w/I");1u("<I><1q></1q><k 1s",\'1Y="1r.o.14.K(\\\'\'+a+\'\\\');">\',a+"</k></I>");25()}}2 1m(a){3=a;c.p("n",a)}c.K=2(){c.K=1m};2 1l(a){4(3!=a){3=a;J(a)}7 q};c.9=2(a){11();c.9=1l;7 c.9(a)};c.12("n",2(a){e(a)})}}C.W=1j o.Q("n");7 1j C()})();',62,130,'||function|_currentHash|if||document|return|var|addHistory||||||||this||_listeners|body|length|_form|historyChange|unFocus|notifyListeners|true|_createAnchor|getScrollY|setAttribute|else|_intervalID|createElement|_historyStates|_recentlyAdded|window|navigator|for|Keeper|style|_pollInterval|_historyFrameRef|name|history|html|_createHistoryHTML|_updateFromHistory|insertBefore|scrollTop|documentElement|setInterval|firstChild|EventManager|px|_createSafariSetHashForm|addHistorySafari|absolute|position|prototype|||with|_watchHistoryLength|_createHistoryFrame|addEventListener|id|History|false|opera|userAgent|arguments|test|WebKit|MSIE|_checkAnchorExists|top|replace|_watchHash|pageYOffset|self|typeof|new|hash|addHistoryIE|updateFromHistory|appVersion|match|location|head|parent|onl|getScrollX|write|left|text|open|frames|900px|block|iframe|unFocusHistoryFrame|display|200|print|undefined|ActiveXObject|clearInterval|submit|action|get|method|unFocusHistoryForm|form|src|javascript|420|splice|getCurrent|Left|removeEventListener|Top|toString|oad|push|String|eval|number|substring|getElementsByName|close'.split('|'),0,{}))
Ext.namespace("Ait.Aspect");

//
// Aspect Client Interface
//
Ait.Aspect.Viewer = function() {
    return Ext.apply(new Ext.util.Observable(), {

        _viewport: null,
        _headerPanel: null, _footerPanel: null,
        _homePanel: null, _contentPanel: null, _searchResultsPanel: null,

        _isAnimationEnabled: true,
        user: null,
        currentPageId: null,
        currentBookTocId: null,
        currentVersion: 1,
        _googleAnalyticsAccId: "",
        _disableHistoryListener: false,

        _history: [], _historyIndex: -1,

        _DEFAULT_LOAD_MASK_DELAY: 500,
        _loadMaskDelay: this._DEFAULT_LOAD_MASK_DELAY,

        //
        // Initialize content viewer app.
        //
        init: function(config) {
            var nsConfigUI = Ait.Aspect.EngineWeb2.Config.UI;

            this.addEvents({
                topicSelected: true,
                topicUnselected: true
            });

            this.user = config.user;

            this.homeHash = "";
            var urlHash = window.location.hash;
            var url = window.location.href;

            // Check if the request from SharePoint webpart 
            if (urlHash && (url.indexOf("f=") != -1) && (url.indexOf("shpoint=") != -1))
                this.homeHash = urlHash.substr(1); //lose the hash symbol
            else if (config.homePage) {
                var hashInx = config.homePage.indexOf("#");
                if (hashInx >= 0 && hashInx < config.homePage.length - 1)
                    this.homeHash = config.homePage.substr(hashInx + 1);
            }

            this._googleAnalyticsAccId = config.googleAnalyticsAccId;

            this._loadMaskDelay = (config.loadMaskDelay) ? config.loadMaskDelay : _DEFAULT_LOAD_MASK_DELAY;

            this._footerPanel = new Ext.BoxComponent({
                region: 'south',
                el: 'footerPanel',
                height: 20,
                hidden: !nsConfigUI.ShowFooter
            });

            Ait.Aspect.HeaderToolbar.init({
                favorites: config.favorites,
                showOptions: (config.filterCriteria && config.filterCriteria.length > 0)
            });

            var headerHeight = nsConfigUI.ShowHeader ? 50 : 0;
            var headerToolbarHeight = nsConfigUI.ShowToolbar ? Ait.Aspect.HeaderToolbar.getHeaderToolbarHeight() : 0;

            this._headerPanel = new Ext.Panel({
                region: 'north',
                height: headerHeight + headerToolbarHeight,
                hidden: !nsConfigUI.ShowHeader && !nsConfigUI.ShowToolbar,
                items: [
                    new Ext.BoxComponent({
                        el: 'headerPanel',
                        height: headerHeight,
                        hidden: !nsConfigUI.ShowHeader
                    }),
                    Ait.Aspect.HeaderToolbar.getHeaderToolbar()]
            });

            Ait.Aspect.HeaderToolbar.on('print', this.print, this);
            Ait.Aspect.HeaderToolbar.on('back', this.back, this);
            Ait.Aspect.HeaderToolbar.on('forward', this.forward, this);
            Ait.Aspect.HeaderToolbar.on('home', this.home, this);
            Ait.Aspect.HeaderToolbar.on('search', this.search, this);
            Ait.Aspect.HeaderToolbar.on('favorite_navigate', this._navigateToPersistentReference, this);
            Ait.Aspect.HeaderToolbar.on('options', this.options, this);

            Ait.Aspect.NavigationTree.init({ rootToc: config.rootToc });

            Ait.Aspect.NavigationTree.on('navigate', function(args) {
                this.navigate(
                        args.pageId,
                        args.topicId,
                        args.treeNodeId,
                        args.treeNodeType,
                        false);
            }, this);

            Ait.Aspect.HomePanel.init();
            Ait.Aspect.ContentPanel.init({ feedbackEmail: config.feedbackEmail, pageEmail: config.pageEmail, editLinks: config.editLinks });

            Ait.Aspect.SearchQuery.SetDefaultBooks(config.books);
            Ait.Aspect.SearchResultsPanel.init({ books: config.books });
            Ait.Aspect.SearchResultsPanel.on('search', function() { this._centerPanel.el.mask('Searching...'); }, this)
            Ait.Aspect.SearchResultsPanel.on('search_complete', function(searchQuery) { this._centerPanel.el.unmask(); }, this)
            Ait.Aspect.SearchResultsPanel.on('result_selected', function(args) {
                this.navigate(
                        args.pageId,
                        args.topicId,
                        args.bookTocId,
                        Ait.Aspect.TocEntryType.Book,
                        true);
            }, this)

            this._centerPanel = new Ext.TabPanel({
                region: 'center',
                deferredRender: false,
                activeTab: 0,
                items: [Ait.Aspect.HomePanel.getPanel(), Ait.Aspect.ContentPanel.getPanel(), Ait.Aspect.SearchResultsPanel.getPanel()]
            });

            // Remove gradient background image from homePanel and contentPanel if Toolbar is hidden
            if (!nsConfigUI.ShowToolbar) {
                var homePanel = Ext.get("homePanel");
                var contentPanel = Ext.get("contentPanel");
                if (homePanel)
                    homePanel.setStyle("background-image", "url()");

                if (contentPanel)
                    contentPanel.setStyle("background-image", "url()");
            }

            this._viewport = new Ext.Viewport({
                layout: 'border',
                items: [
                    this._headerPanel,
                    this._footerPanel,
                    Ait.Aspect.NavigationTree.getTreePanel(),
                    this._centerPanel]
            });

            this.showCenterPanel(Ait.Aspect.HomePanel);

            // Show the div 'headerPanel' (the display style set to 'dosplay: none;' on default.aspx page to avoid flashing onload)
            // can be noticed on firefox than ie
            Ext.get("headerPanel").setStyle("display", "block");

            // Init options dialog
            Ait.Aspect.OptionsDialog.init({ user: this.user, filterCriteria: config.filterCriteria });
            Ait.Aspect.OptionsDialog.on('options_saved', function() {
                this.refresh();
            }, this);

            // Init history  
            this.initHistory();
        },

        //
        // Navigate to a page.
        //
        navigate: function(pageId, topicId, treeNodeId, treeNodeType, syncTree) {

            if (syncTree == true) {
                // Select node in tree
                Ait.Aspect.NavigationTree.selectNode(Ait.Aspect.TocNodeId.get(treeNodeId, treeNodeType));

                // This will fire event to navigate to page...
                return;
            }

            if (treeNodeType == Ait.Aspect.TocEntryType.Site) {
                // This is a site node, no associated page
                this.showCenterPanel(Ait.Aspect.ContentPanel);
                this._addNodeToHistory(treeNodeId, Ait.Aspect.TocEntryType.Site);
                Ait.Aspect.ContentPanel.displaySitePage(Ait.Aspect.NavigationTree.getTrail());

                this.currentPageId = pageId;
                this.currentBookTocId = null;
                this.fireEvent('topicUnselected');

                //now we want to move the user to the next node because we don't like selecting sites
                Ait.Aspect.NavigationTree.moveNext();
            }
            else {
                if (pageId == this.currentPageId) {
                    // We're already on correct page, just select topic
                    this._navigateToTopic(topicId, treeNodeId);
                    // Rerender Breadcrumb Trail
                    Ait.Aspect.ContentPanel._renderBreadcrumbTrail(Ait.Aspect.NavigationTree.getTrail());
                    return;
                }

                //applyMask determines whether or not to show the mask after the defered period
                this._centerPanel.applyMask = true;
                var applyMask = function() {
                    if (this._centerPanel.applyMask === true)
                        this._centerPanel.el.mask('Loading...');
                } .createDelegate(this);
                applyMask.defer(this._loadMaskDelay);

                // This is a node in a book, which has an associated page
                Ait.Aspect.Services.Page.get(pageId, function(page) {

                    this.showCenterPanel(Ait.Aspect.ContentPanel);
                    Ait.Aspect.ContentPanel.getCSS(page, function() {

                        Ait.Aspect.ContentPanel.displayPage(page, Ait.Aspect.NavigationTree.getTrail());
                        this.currentPageId = page.Id;

                        this._centerPanel.applyMask = false;
                        this._centerPanel.el.unmask();

                        this._navigateToTopic(topicId, treeNodeId);

                    } .createDelegate(this));
                }, this,
                    function(exceptionMessage) {
                        // Exception getting page.
                        this._centerPanel.applyMask = false;
                        this._centerPanel.el.unmask();
                        Ext.MessageBox.alert("Error", "Page could not be retrieved.  Please try again.");
                    }, this);
            }
        },
        
        _recordSearchAnalytics: function(searchQuery) {
        
            var pageTracker = _gat._getTracker(this._googleAnalyticsAccId);
            try {
                pageTracker._setCustomVar(3, "Search", searchQuery.text, 3);
                pageTracker._trackPageview("Search");
            }
            catch (err) {
                err.description;
            }
        },

        _recordNavigationAnalytics: function(treeNodeId) {

            if (!this._googleAnalyticsAccId) return;

            var tocNodeId = Ait.Aspect.TocNodeId.get(treeNodeId, Ait.Aspect.TocEntryType.Book);
            var persistentReference = Ait.Aspect.NavigationTree.getPersistentReference(tocNodeId);
            var pageHash = persistentReference.getHash();
            if (!pageHash) return;

            var url = document.location.pathname + '#' + pageHash;

            var nodeInfo = Ait.Aspect.NavigationTree.getNodeInfo(tocNodeId);
            if (nodeInfo && nodeInfo.text)
                url = nodeInfo.text;

            var pageTracker = _gat._getTracker(this._googleAnalyticsAccId);
            try {
                pageTracker._setCustomVar(1, "BookID", persistentReference.libraryBookId, 3);
                pageTracker._setCustomVar(2, "TopicID", persistentReference.libraryTopicId, 3);
                pageTracker._trackPageview(url);
            }
            catch (err) {
                err.description;
            }

        },

        //
        // Navigate to topic
        //
        _navigateToTopic: function(topicId, treeNodeId) {

            this._recordNavigationAnalytics(treeNodeId);

            this._addNodeToHistory(treeNodeId, Ait.Aspect.TocEntryType.Book);

            this.showCenterPanel(Ait.Aspect.ContentPanel);
            Ait.Aspect.ContentPanel.scrollToTopic(topicId);

            this.currentBookTocId = treeNodeId;

            if (this.currentBookTocId)
                this.fireEvent('topicSelected', { currentBookTocId: this.currentBookTocId });
        },

        //
        // Print the current page.
        //
        print: function() {
            window.print();
        },

        //
        // Navigate back
        //
        back: function() {
            window.history.back();
        },

        //
        // Navigate forward
        //
        forward: function() {
            window.history.forward();
        },

        //
        // Navigate home
        //
        home: function() {

            if (this.homeHash) {
                this._executeHashOption(this.homeHash, null);
                return;
            }

            this.addHistory('');
            this.showCenterPanel(Ait.Aspect.HomePanel);
            Ait.Aspect.NavigationTree.clearSelection();
        },

        //
        // Search
        //
        search: function(searchQuery) {

            this.showCenterPanel(Ait.Aspect.SearchResultsPanel);
            Ait.Aspect.NavigationTree.clearSelection();
            Ait.Aspect.SearchResultsPanel.search(searchQuery);
            this._recordSearchAnalytics(searchQuery);
            this.addHistory(searchQuery.getHash());
        },

        //
        // Show a panel in main area, hide other panels.
        //
        showCenterPanel: function(panel) {
            this._centerPanel.activate(panel.getPanel());
        },

        //
        // Service exception handler.
        // 
        _serviceException: function() {
            Ext.MessageBox.alert("Service Exception", "Service exception occurred.");
        },

        //
        // Initialize history
        //
        initHistory: function() {
            unFocus.History.addEventListener('historyChange', this._historyListener.createDelegate(this));
            this._historyListener(unFocus.History.getCurrent());
        },

        _addNodeToHistory: function(nodeId, nodeType) {
            var tocNodeId = Ait.Aspect.TocNodeId.get(nodeId, nodeType);
            var persistentReference = Ait.Aspect.NavigationTree.getPersistentReference(tocNodeId);
            this.addHistory(persistentReference.getHash());
        },

        //
        // Add history point
        //
        addHistory: function(hash) {

            hash = this._fixFFSpaceReplacement(hash);

            if (this._disableNextAddHistory == true) {
                this._disableNextAddHistory = false;
                return;
            }

            this._disableHistoryListener = true;

            unFocus.History.addHistory(hash);

            // Move in internal history
            this._historyIndex++;

            // If we're not add the end of history, remove all entries after current entry
            if (this._historyIndex < this._history.length) {
                this._history = this._history.slice(0, this._historyIndex);
            }

            this._history[this._historyIndex] = hash;
            this._setBackForwardEnabled();

            this._disableHistoryListener = false;
        },

        //
        // History event handler
        //
        _historyListener: function(hash) {
            if (this._disableHistoryListener == true)
                return;

            // Have we gone backward, or forward?
            // (for internal history)
            var isHistoryNavigate = false;
            if (this._history[this._historyIndex - 1] == hash) {
                // We've gone back
                isHistoryNavigate = true;
                this._historyIndex--;
            }
            else if (this._history[this._historyIndex + 1] == hash) {
                // We've gone forward
                isHistoryNavigate = true;
                this._historyIndex++;
            }

            this._setBackForwardEnabled();

            // Navigate to location specified by history
            this._disableNextAddHistory = isHistoryNavigate;

            this._executeHashOption(hash, this.home.createDelegate(this));
        },

        _executeHashOption: function(hash, defaultAction) {

            if (!hash) {
                if (defaultAction) defaultAction();
                return;
            }

            //legacy conversion
            if (hash.indexOf("code") == 0) hash = hash.replace("code", "t");

            var firstCh = hash.charAt(0).toLowerCase();
            if (Ait.Aspect.SearchQuery.HashPrefixMatch(firstCh))
                this.search(Ait.Aspect.SearchQuery.FromHash(hash));
            else if (Ait.Aspect.PersistentReference.HashPrefixMatch(firstCh))
                this._navigateToPersistentReference(Ait.Aspect.PersistentReference.FromHash(hash), defaultAction);
            else if (defaultAction)
                defaultAction();
        },

        _navigateToPersistentReference: function(persistentReference, onFailure) {

            Ait.Aspect.NavigationTree.getNodeIdFromPersistentReference(persistentReference, function(treeNodeId) {

                if (treeNodeId)
                    Ait.Aspect.NavigationTree.selectNode(treeNodeId);
                else if (onFailure)
                    onFailure();

            } .createDelegate(this));
        },


        //
        // Set back/forward buttons enabled
        //
        _setBackForwardEnabled: function() {
            Ait.Aspect.HeaderToolbar.setBackEnabled(this._historyIndex > 0);
            Ait.Aspect.HeaderToolbar.setForwardEnabled(this._historyIndex < this._history.length - 1);
        },

        //
        // Choose user options
        //
        options: function() {
            Ait.Aspect.OptionsDialog.show();
        },

        //
        // Refresh toc & page
        //
        refresh: function() {
            if (!isNaN(this.currentPageId) && !isNaN(this.currentBookTocId)) {
                var pageId = new Number(this.currentPageId);
                var bookTocId = new Number(this.currentBookTocId);

                this.currentPageId = null;
                this.currentBookTocId = null;
                this.fireEvent('topicUnselected');

                Ait.Aspect.NavigationTree.refresh(pageId, bookTocId, Ait.Aspect.TocEntryType.Book);

                // Refresh search if it is open
                if (Ait.Aspect.SearchResultsPanel.getPanel().isVisible())
                    Ait.Aspect.SearchResultsPanel.searchAgain();
            }
        },

        //
        // Firefox 3 and upwards has a feature which replaces the %20 in the address ('awesome') bar
        // with a space. Unfortunately to the history event that we handle, this behaviour looks like 
        // two different addresses and results in the search looping infinitely.
        //
        // In general I prefer 'browser specific' features be done by feature detection (testing for 
        // the existence of a method or property) however I haven't found a feature to detect in this
        // case so we are forced back to browser detection.
        //
        _fixFFSpaceReplacement: function(hash) {
        
            if(!Ext.isGecko3) return hash; //only FF3 and up
            
            return hash.replace(/%20/g, ' ');
        }
    });
} ();
Ext.namespace("Ait.Aspect");

Ext.QuickTips.init();


//
// Header toolbar
//
Ait.Aspect.HeaderToolbar = function() {
    return Ext.apply(new Ext.util.Observable(), {

        _headerToolbar: null,
        _btnHome: null, _btnBack: null, _btnForward: null,
        _btnFavorites: null, _btnPrint: null, _btnOptions: null,
        _txtSearch: null, _btnSearch: null,
        _headerToolbarHeight: 27,
        _reference: null, _favorites: null, //last selected bookTocId from Viewer, and last loaded favorites

        // Initialize header toolbar.
        init: function(config) {
            this.addEvents({
                print: true,
                back: true,
                forward: true,
                next: true,
                previous: true,
                home: true,
                search: true,
                favorite_navigate: true,
                options: true
            });

            this._btnHome = new Ext.Toolbar.Button({
                text: 'Home',
                cls: 'x-btn-text-icon',
                iconCls: 'homeIcon'
            });
            this._btnHome.on('click', function() { this.fireEvent('home') }, this);

            this._btnBack = new Ext.Toolbar.Button({
                text: 'Back',
                cls: 'x-btn-text-icon',
                iconCls: 'backIcon',
                disabled: true
            });
            this._btnBack.on('click', function() { this.fireEvent('back') }, this);

            this._btnForward = new Ext.Toolbar.Button({
                text: 'Forward',
                cls: 'x-btn-text-icon',
                iconCls: 'forwardIcon',
                disabled: true
            });
            this._btnForward.on('click', function() { this.fireEvent('forward') }, this);

            this._btnNext = new Ext.Toolbar.Button({
                text: 'Next',
                cls: 'x-btn-text-icon',
                iconCls: 'nextIcon',
                disabled: true
            });
            this._btnNext.on('click', function() { this.fireEvent('next') }, this);

            this._btnPrevious = new Ext.Toolbar.Button({
                text: 'Previous',
                cls: 'x-btn-text-icon',
                iconCls: 'previousIcon',
                disabled: true
            });
            this._btnPrevious.on('click', function() { this.fireEvent('previous') }, this);

            this._btnFavorites = new Ext.Toolbar.Button({
                text: 'Favorites',
                cls: 'x-btn-text-icon',
                iconCls: 'favoritesIcon'
            });
            this._createFavoritesMenu(config.favorites);

            this._btnPrint = new Ext.Toolbar.Button({
                text: 'Print',
                cls: 'x-btn-text-icon',
                iconCls: 'printIcon'
            });
            this._btnPrint.on('click', function() { this.fireEvent('print') }, this);

            this._btnOptions = new Ext.Toolbar.Button({
                text: 'Options',
                cls: 'x-btn-text-icon',
                iconCls: 'optionsIcon'
            });
            this._btnOptions.on('click', function() { this.fireEvent('options') }, this);

            this._txtSearch = new Ext.form.TextField({
                width: 150,
                validator: this._validateSearch
            });
            this._txtSearch.on('specialkey', this._txtSearch_specialkey, this);

            /* Search Auto-Complete.
            This code is completely functional, and was only removed because it became uneconomic
            to perform a content-filtered search on the serverside.
            
            var suggestionsProxy = new Ait.Web.JsonService.Proxy({ url: 'Services/Search.asmx/GetSuggestions' });
            suggestionsProxy.on('beforeload', function(_, params) {
            params["limit"] = 10;
            Ext.Ajax.request({
            url: suggestionsProxy.url,
            headers: { 'Content-Type': 'application/json' },
            method: 'POST',
            params: Ext.encode(params),
            scope: this,
            success: function(response) {
            //make ajax request and process data.. then load manually into the store.
            var jsonData = Ext.decode(response.responseText);
            var values = [];
            for (var i = 0, ci = jsonData.Value.length; i < ci; i++)
            values.push([jsonData.Value[i]]);
            this._txtSearch.store.loadData(values);
            }
            });
            return false;
            }, this);

            this._txtSearch = new Ext.form.ComboBox({
            store: new Ext.data.SimpleStore({
            proxy: suggestionsProxy,
            fields: ['suggestion']
            }),
            displayField: 'suggestion',
            typeAhead: false,
            loadingText: 'Searching...',
            width: 150,
            hideTrigger: true,
            tpl: new Ext.XTemplate('<tpl for="."><div class="search-item" style="padding: 2px;">{suggestion}</div></tpl>'),
            itemSelector: 'div.search-item',
            onSelect: function(record) {
            this._txtSearch.setValue(record.get("suggestion"));
            } .createDelegate(this)
            });*/

            this._btnSearch = new Ext.Toolbar.Button({
                text: 'Search',
                cls: 'x-btn-text-icon',
                iconCls: 'searchIcon'
            });
            this._btnSearch.on('click', function() { this._search() }, this)

            var toolbarItems = [this._btnHome, this._btnBack, this._btnForward, this._btnNext, this._btnPrevious, '-', this._btnFavorites, this._btnPrint];

            if (config.showOptions === true)
                toolbarItems.push(this._btnOptions);

            toolbarItems = toolbarItems.concat(['->', this._txtSearch, this._btnSearch]);

            this._headerToolbar = new Ext.Toolbar({
                items: toolbarItems,
                style: 'border-bottom: 0px',
                hidden: !Ait.Aspect.EngineWeb2.Config.UI.ShowToolbar
            });
        },

        //
        // Handle enter key press
        //
        _txtSearch_specialkey: function(field, evt) {
            if (evt.getKey() == Ext.EventObject.ENTER) {
                this._search();
            }
        },

        _validateSearch: function(searchString) {
            if (searchString.indexOf("*") == 0 || searchString.indexOf("?") == 0)
                return "Search cannot start with a wildcard (* or ?)";
            return true;
        },

        //
        // Raise search event if search term valid
        //
        _search: function() {
            var searchString = this._txtSearch.getValue().trim();

            if (this._validateSearch(searchString) !== true)
                return;

            if (searchString.length == 0)
                return;
                
            this.fireEvent('search', new Ait.Aspect.SearchQuery({ 
                text: searchString,
                bookIds: Ait.Aspect.SearchResultsPanel.getSelectedBookIds()
            }));
        },

        //
        // Get favorites menu items.
        //
        _getFavoritesMenuItems: function(favorites) {
            this._favorites = favorites;
            var items = [];

            if (favorites != null) {
                for (var i = 0; i < favorites.length; i++) {
                    var favorite = favorites[i];

                    items.push(new Ait.Aspect.FavoriteMenuItem({
                        text: favorite.Title,
                        favoriteId: favorite.Id,
                        reference: favorite.Reference,
                        handler: this._favorite_click.createDelegate(this),
                        iconCls: 'favoriteIcon',
                        cls: 'favoritesItem'
                    }));
                }
            }

            this._btnAddFavorite = new Ext.menu.Item({
                text: 'Add Favorite',
                handler: this._addFavorite_click.createDelegate(this),
                iconCls: 'addFavoriteIcon',
                disabled: this._isAddFavoritesDisable()
            });

            Ait.Aspect.Viewer.on('topicUnselected', function() {
                this._reference = '';
                this._btnAddFavorite.setDisabled(this._isAddFavoritesDisable());
            }, this);

            Ait.Aspect.Viewer.on('topicSelected', function() {
                var treeNodeId = Ait.Aspect.NavigationTree.getSelectedTreeNodeId();
                this._reference = Ait.Aspect.NavigationTree.getPersistentReference(treeNodeId).getHash();
                this._btnAddFavorite.setDisabled(this._isAddFavoritesDisable());
            }, this);

            items.push('-');
            items.push(this._btnAddFavorite);
            return items;
        },

        //
        // Get desired state of add favorite button based on content of the items
        // and currently selected bookTocId. Returns true if AddFavorites should be disabled.
        //
        _isAddFavoritesDisable: function() {
            //no topic selected: can't add nothing as a bookmark
            var treeNodeId = Ait.Aspect.NavigationTree.getSelectedTreeNodeId();
            if(!treeNodeId) return true;

            //no favorites then nothing to stop us adding this topic
            if (!this._favorites) return false;

            //disable if is reference already in the favorites
            for (var i = 0, ci = this._favorites.length; i < ci; i++)
                if (this._favorites[i].Reference == this._reference)
                return true; //match found - so disable

            //not found - so add favorites can be enabled
            return false;
        },

        //
        // Add favorite clicked
        //
        _addFavorite_click: function() {
        
            var trail = Ait.Aspect.NavigationTree.getTrail();
            if(!trail || trail.length == 0) return;
            
            var leaf = trail.pop();

            Ait.Aspect.Services.Favorite.add(leaf.hash, leaf.name, function(favorites) {
                this.refreshFavorites(favorites);
            }, this);
        },

        //
        // Favorite clicked
        //
        _favorite_click: function(menuItem, evt) {
            if (!menuItem.reference) return;
            
            var pr = Ait.Aspect.PersistentReference.FromHash(menuItem.reference);
            if(!pr) return;
            
            this.fireEvent('favorite_navigate', pr);
        },

        //
        // Create Favorites
        // Unfortunately Ext 2.2.1 doesn't support doLayout on the menu, so to fix the
        // shadow rendering we have ot redraw the menu.
        //
        _createFavoritesMenu: function(favorites) {

            if (this._btnFavorites.menu) {
                this._btnFavorites.menu.removeAll();
                this._btnFavorites.menu.destroy();
            }

            this._btnFavorites.menu = new Ext.menu.Menu({
                items: this._getFavoritesMenuItems(favorites)
            });
            //Ext has some code which forces the menu width, but that code is not helping us, so this undoes it:
            this._btnFavorites.menu.getEl().setStyle("width", "auto");
        },

        //
        // Refresh favorites
        //
        refreshFavorites: function(favorites) {
            if (favorites != null) {
                this._createFavoritesMenu(favorites);
                return;
            }

            // Get favorites
            Ait.Aspect.Services.Favorite.get(function(newFavorites) {
                this._createFavoritesMenu(newFavorites);
            }, this);
        },

        //
        // Set back button enabled
        //
        setBackEnabled: function(isEnabled) {
            this._btnBack.setDisabled(!isEnabled);
        },

        //
        // Set forward button enabled
        //
        setForwardEnabled: function(isEnabled) {
            this._btnForward.setDisabled(!isEnabled);
        },

        setNextEnabled: function(isEnabled) {
            this._btnNext.setDisabled(!isEnabled);
        },

        setPreviousEnabled: function(isEnabled) {
            this._btnPrevious.setDisabled(!isEnabled);
        },

        getHeaderToolbar: function() {
            return this._headerToolbar;
        },

        getHeaderToolbarHeight: function() {
            return this._headerToolbarHeight;
        }

    });
} ();


//
// Favorite menu item
//
Ait.Aspect.FavoriteMenuItem = function(config) {  
    Ait.Aspect.FavoriteMenuItem.superclass.constructor.call(this, config);
};

Ext.extend(Ait.Aspect.FavoriteMenuItem, Ext.menu.Item, {
    onRender: function(container, position) {
        Ait.Aspect.FavoriteMenuItem.superclass.onRender.call(this, container, position);

        var removeImg = document.createElement("img");
        removeImg.src = Ext.BLANK_IMAGE_URL.replace(/&amp;/gi, "&");
        removeImg.title = 'Remove Favorite';
        removeImg.className = 'removeFavoriteIcon';
        this.el.appendChild(removeImg);

        Ext.get(removeImg).on('click', function(e) {
            // Don't let click bubble
            e.stopEvent();

            // Delete favorite
            Ait.Aspect.Services.Favorite.remove(this.favoriteId, function(favorites) {
                Ait.Aspect.HeaderToolbar.refreshFavorites(favorites);
            }, this);            
        }, this);
    }
});
//
// The XHtmlRenderer for Aspect generates anchor tags with special attributes that 
// describe how the link is to operate.  These classes handle the construction and
// operation of those links.
//
Ext.namespace('Ait.Aspect');

Ait.Aspect.LinkFactory = {
    //
    // Create a hyperlink based on the hyperlinkType attribute
    //
    Create: function(link) {
        if (link == null) return;

        var linktype = link.getAttribute("hyperlinkType");

        switch (linktype) {
            case '1': return new Ait.Aspect.LinkSameWindow(link);
            case '2': return new Ait.Aspect.LinkNewWindow(link);
            case '3': return new Ait.Aspect.LinkPopup(link);
            case '4': return new Ait.Aspect.LinkExpand(link);
            case '5': return new Ait.Aspect.LinkExpand(link);
            case '6': return new Ait.Aspect.LinkPopup(link);
            case '7': return new Ait.Aspect.LinkPopup(link, true);
        }
    }
}

//
// Ait.Aspect.LinkBase
// Base class for all the links
//
Ait.Aspect.LinkBase = function(link) {
    this._link = link;
    this._targetEle = null; //The Ext.Element that is affect by the link action for types 4 - 7.

    //contentId attribute is only required for some link types
    this._contentId = link.getAttribute("contentId"); //e.g. d1234

    if (this._contentId != null) {
        this._rawContentId = this._contentId.substr(1); //e.g. 1234
        this._targetEle = new Ext.Element.get("d" + this._rawContentId);
    }

    //linktargets attribute is only required for link types 1 and 2
    this._linkTargets = this.parseLinkTargets();
    Ait.Aspect.LinkSelectionPopup.Create(this._link, this._linkTargets);
}

Ait.Aspect.LinkBase.prototype = {

    on: function(name, handler) {
        Ext.fly(this._link).on(name, handler, this);
    },

    //
    // Creates a link selection popup if one is required based on the link attributes.
    //
    // The parsing expects an anchor tag that looks something like:
    //  <a  topicid="7952" 
    //      linktargets="[{pageId:3572,bookTocId:4244,book:'AspectTest2'},{pageId:3573,bookTocId:4248,book:'AspectTest'}]" 
    //      title="..." href="#" hyperlinktype="1" id="h209">...</a>
    //
    parseLinkTargets: function(link) {
        var attrLinkTargets = this._link.getAttribute("linktargets");
        if (!attrLinkTargets) return null;

        //attrLinkTargets = attrLinkTargets.replace(/&apos;/gi, "'"); //because some xml parsers don't to this automatically.

        //This is a little dangerous.  We are assuming that the people publishing content are 'safe' users
        //and won't put arbitrary javascript in the title of their books.
        var linkTargets = null;
        try { linkTargets = eval(attrLinkTargets); } catch (ex) { }

        if (!linkTargets || !linkTargets.length || linkTargets.length == 0) return null; //invalid data
        return linkTargets;
    }
}

//
// hyperlinktype="1": Same Window uses standard HTML <A HREF> tags to display the destination topic in the same window. 
// The target topic must be included in the published output for the link to be created.
//
Ait.Aspect.LinkSameWindow = function(link) {
    Ait.Aspect.LinkSameWindow.superclass.constructor.call(this, link);
    this.on('click', this._onClick);
}

Ext.extend(Ait.Aspect.LinkSameWindow, Ait.Aspect.LinkBase, {

    _onClick: function(ev) {

        ev.stopEvent();

        var siteTocId = this._link.getAttribute("siteTocId");
        if (siteTocId) {
            Ait.Aspect.Viewer.navigate(null, null, siteTocId, Ait.Aspect.TocEntryType.Site, true);
            return;
        }

        var pageId, bookTocId;
        if (this._linkTargets && this._linkTargets.length > 0) {
            pageId = this._linkTargets[0].pageId;
            bookTocId = this._linkTargets[0].bookTocId;
        }

        if (pageId != null)
            Ait.Aspect.Viewer.navigate(pageId, this._link.getAttribute("topicId"), bookTocId, Ait.Aspect.TocEntryType.Book, true);
        else
            location.href = this._link.href;
    },

    toString: function() { return "Ait.Aspect.LinkSameWindow"; }
});

//
// hyperlinktype="2": New Window uses standard HTML <A HREF> tags to display the destination topic in a new window. 
// The target topic must be included in the published output for the link to be created.
//
Ait.Aspect.LinkNewWindow = function(link){
    Ait.Aspect.LinkNewWindow.superclass.constructor.call(this, link);
    this.on('click', this._onClick);
}

Ext.extend(Ait.Aspect.LinkNewWindow, Ait.Aspect.LinkBase, {

    _onClick: function(ev) {

        var siteTocId = this._link.getAttribute("siteTocId");
        var bookTocId = (this._linkTargets && this._linkTargets.length > 0) ? this._linkTargets[0].bookTocId : null;
        var sharepointPageURL = getQueryStringValueFromUrl(window.location.href, "shpoint");
        
        if (siteTocId)
            window.open("default.aspx#s" + siteTocId);
        else if (bookTocId != null && sharepointPageURL) {
        
            sharepointPageURL = removeQueryStringsFromUrl(sharepointPageURL, ["content"]);
            var contentIdQueryString = "content=b" + bookTocId;

            if (sharepointPageURL.indexOf("?") > 0) //Check for QueryString
                sharepointPageURL += "&" + contentIdQueryString;
            else 
                sharepointPageURL += "?" + contentIdQueryString;

            window.open(sharepointPageURL);
        }
        else if (bookTocId != null)
            window.open("default.aspx#b" + bookTocId);
        else
            window.open(this._link.href);

        ev.stopEvent();
    },

    toString: function() { return "Ait.Aspect.LinkNewWindow"; }
});


//
// hyperlinktype="4": Expanding Block uses Dynamic HTML (DHTML) to show and hide (or "expand" and "collapse") the fully formatted text of the destination topic. 
// Clicking the link will make the topic expand and appear in a block below the paragraph containing the anchor text, 
// and clicking it once more will make it disappear again. The target topic does not need to be included in the published output.
//
// hyperlinktype="5": Expanding Inline uses Dynamic HTML (DHTML) to show and hide (or "expand" and "collapse") the fully formatted 
// text of the destination topic. Clicking the link will make the topic appear inline and in the same paragraph immediately 
// following the anchor text, and clicking it once more will make it disappear again. The target topic does not need to be included in the published output.
// Note: If the destination topic contains multiple paragraphs, only the first paragraph is used.
//
Ait.Aspect.LinkExpand = function(link) {
    Ait.Aspect.LinkExpand.superclass.constructor.call(this, link);
    if (!this._targetEle) return;

    this._targetEle.setVisibilityMode(Ext.Element.DISPLAY);
    this.on('click', this._onClick);
}

Ext.extend(Ait.Aspect.LinkExpand, Ait.Aspect.LinkBase, {

    _onClick: function(ev) {

        ev.stopEvent();
        if (!this._targetEle) return;

        //Toggle target visibility
        this._targetEle.setVisible(!this._targetEle.isVisible());

        //Toggle expanded class:
        this._link.className = (!this._link.className) ? "expanded" : "";
    },

    toString: function() { return "Ait.Aspect.LinkExpand"; }
});


//
// hyperlinktype="6" Popup on Click uses Dynamic HTML (DHTML) to display the fully formatted text of the destination topic in a popup window. 
// The popup is activated on clicking, and remains open until closed. It can be closed by either clicking the link again, 
// or by clicking the close button in the top right corner. The target topic does not need to be included in the published output.
//
// hyperlinktype="7" Popup on Hover uses Dynamic HTML (DHTML) to display the fully formatted text of the destination topic in a popup window. 
// The popup is displayed when the mouse is hovered over the anchor text, and disappears when the mouse is moved away.
//
// hyperlinktype="3": Standard Popup in HTML, XHTML, and HTML Help, a standard popup uses the HTML Help ActiveX control to 
// display the destination topic in a popup window. In JavaHelp and Oracle Help for Java, it uses the native popup functionality of those technologies. 
// The target topic does not need to be included in the published output. Note: Standard popups are text-only and do not display character formatting, tables or images.
// The Aspect version of this does NOT do the above.  At present the published output results a hyperlink, but nothing for it to
// popup so, as the content element is missing, this won't actually do anything. This is actually 'better' than the XHTML output from Author-it which just dumps the hyperlink.
//
Ait.Aspect.LinkPopup = function(link, isHover) {
    Ait.Aspect.LinkPopup.superclass.constructor.call(this, link);

    if (!this._targetEle) return;
    this._linkEle = new Ext.Element(this._link);

    //Change to using visibility so we can use the offsets.
    //Assumption is that targetEle and link share the same offsetParent.
    //This won't affect layout as the styles for these popups are position:absolute.
    this._targetEle.setVisibilityMode(Ext.Element.VISIBILITY);
    this._targetEle.hide();
    this._targetEle.setStyle("display", "block"); //over-ride the style setting in the html

    if (isHover === true) {
        this.on('mouseover', this._onMouseOver);
        this.on('mouseout', this._onMouseOut);
    }
    else {
        this._addCloseButton();
        this.on('click', this._onClick);
    }
}

Ext.extend(Ait.Aspect.LinkPopup, Ait.Aspect.LinkBase, {

    _onClick: function(ev) {
        ev.stopEvent();

        //Toggle target visibility
        var newVisibility = !this._targetEle.isVisible();
        if (newVisibility) this._setPosition();
        this._targetEle.setVisible(newVisibility);
    },

    _onMouseOver: function(ev) {
        ev.stopEvent();
        this._setPosition();
        this._targetEle.show();
    },

    _onMouseOut: function(ev) {
        ev.stopEvent();
        this._targetEle.hide();
    },

    //
    // Positions the popup underneath the link and an arbitrary offset from the link (currently 10px to the left, 2px below)
    //
    _setPosition: function() {
        var offsets = this._linkEle.getOffsetsTo(this._linkEle.dom.offsetParent);
        var offsetX = offsets[0] + 10; //10 is arbitrary offset to the left of the link
        var offsetY = offsets[1] + this._linkEle.getHeight() + 2; //2 is arbitrary offset from the bottom of the link
        this._targetEle.setLeftTop(offsetX, offsetY);
    },
    
    _addCloseButton: function() {
        var btnClose = this._targetEle.createChild({tag: 'div', cls: 'popupCloseButton', html: ''}, this._targetEle.first(null, true));
        btnClose.on('click', function() {
            this._targetEle.hide();
        }, this);
    },

    toString: function() { return "Ait.Aspect.LinkPopup"; }
});


//
// Panel intended to be used as a pop-up to show multiple
// destination links when they are available.
//
Ait.Aspect.LinkSelectionPopup = Ext.extend(Ext.ux.PersistentTip,{

    linkTemplate: new Ext.Template(
        '<a href="{href}" hyperlinkType="{hyperlinkType}" topicId="{topicId}" linktargets="{linktargets}" >{name}</a>'
    ),
    
    rowTemplate: new Ext.Template(
        '<div>{link}</div>'
    ),
    
    mainTemplate: new Ext.Template(
        'This link appears in more than one book.<br />',
        'Please choose a book from the following list:<br />',
        '<div style="padding-left: 3px">{rows}</div>'
    ),

    afterRender : function(){
        Ait.Aspect.LinkSelectionPopup.superclass.afterRender.call(this);
        
        this.body.update( this.mainTemplate.apply({
            rows: this.getHtml()
        }) );
        
        var links = this.body.dom.getElementsByTagName("a");
        for (var i = 0, ci = links.length; i < ci; i++) {
            var link = links[i];
            Ait.Aspect.LinkFactory.Create(link);
            Ext.fly(link).on("click", this.hide, this);
        }
    },
    
    getHtml: function() {
        var rowHtml = [];
        var links = this.getHtmlLinks();
        
        for(var i = 0, ci = links.length; i < ci; i++)
            rowHtml.push(this.rowTemplate.apply({ link: links[i] }));
        
        return rowHtml.join("");
    },
    
    getHtmlLinks: function() {
        
        var target = this.initialConfig.target;
        var linkTargets = this.initialConfig.linkTargets;
        
        if(!target || !linkTargets) return;
        
        var links = [];
        for(var i = 0, ci = linkTargets.length; i < ci; i++) {
            var linkTarget = linkTargets[i];

            links.push(this.linkTemplate.apply({
                href: target.href,
                name: linkTarget.book,
                hyperlinkType: target.getAttribute("hyperlinkType"),
                topicId: target.getAttribute("topicId"),
                linktargets: Ext.encode([linkTarget]).replace(/\'/gi,"\\'").replace(/\"/gi, "'")
            }));
        }
        
        return links;
    }
    
});

   
Ait.Aspect.LinkSelectionPopup.Create = function(link, linkTargets) {
 
    if(!linkTargets || !linkTargets.length || linkTargets.length == 0) return null; //invalid data
    if(linkTargets.length <= 1) return null; //link selection popup not required because there are too few links
    
    return new Ait.Aspect.LinkSelectionPopup({
        target: link,
        linkTargets: linkTargets
    }); 
}
Ext.namespace("Ait.Aspect");

//
// Navigation Tree
//
Ait.Aspect.NavigationTree = function() {
    return Ext.apply(new Ext.util.Observable(), {
        _treePanel: null,
        _isAnimationEnabled: true,

        //
        // Initialize
        //
        init: function(config) {
            this.addEvents({
                navigate: true
            });

            this._treePanel = new Ext.tree.TreePanel({
                region: 'west',
                hidden: Ait.Aspect.EngineWeb2.Config.UI.TocAction == Ait.Aspect.EngineWeb2.Config.TocAction.Disabled,
                loader: new Ait.Aspect.TocTreeLoader({
                    url: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/Toc.asmx/GetChildren'
                }),
                el: 'navigationTree',
                width: 300,
                split: true,
                rootVisible: false,
                collapsible: true,
                collapsed: Ait.Aspect.EngineWeb2.Config.UI.TocAction == Ait.Aspect.EngineWeb2.Config.TocAction.Collapsed,
                collapseMode: 'mini',
                animate: this._isAnimationEnabled,
                animCollapse: this._isAnimationEnabled,
                autoScroll: true,
                containerScroll: true,
                margins: '4 0 0 4'
            });

            var rootNode = new Ext.tree.AsyncTreeNode({ id: 's-1', text: 'Root', leaf: false, pageId: -1 });
            this._treePanel.setRootNode(rootNode);
            this._treePanel.getLoader().loadWithChildren(this._treePanel.root, config.rootToc);

            this._treePanel.getSelectionModel().on('selectionchange', this._treePanel_selectionchange, this);

            Ait.Aspect.HeaderToolbar.on('next', this.moveNext, this);
            Ait.Aspect.HeaderToolbar.on('previous', this.movePrevious, this);
        },


        //
        // Get tree panel control
        //
        getTreePanel: function() {
            return this._treePanel;
        },

        //
        // Selection changed.
        //
        _treePanel_selectionchange: function(selModel, node) {

            if (node != null) {
                var idParts = Ait.Aspect.TocNodeId.parse(node.id);

                this.fireEvent('navigate', {
                    pageId: node.attributes.pageId,
                    topicId: node.attributes.topicId,
                    treeNodeId: idParts.id,
                    treeNodeType: idParts.type
                });

                Ait.Aspect.HeaderToolbar.setNextEnabled(node.attributes.next != null);
                Ait.Aspect.HeaderToolbar.setPreviousEnabled(node.attributes.previous != null);

            } else {
                Ait.Aspect.HeaderToolbar.setNextEnabled(false);
                Ait.Aspect.HeaderToolbar.setPreviousEnabled(false);
            }
        },

        moveNext: function() {
            var node = this._treePanel.selModel.selNode;
            if (node == null) return;

            var nextNode = node.attributes.next;
            if (!nextNode) return;
            this.selectNode(nextNode);
        },

        movePrevious: function() {
            var node = this._treePanel.selModel.selNode;
            if (node == null) return;

            var previousNode = node.attributes.previous;
            if (!previousNode) return;
            this.selectNode(previousNode);
        },

        //
        // Get navigation trail to currently selected node
        //
        getTrail: function() {
            // Get all ids until root node
            var currentNode = this._treePanel.selModel.selNode;
            var idsToRoot = [];

            while (currentNode != this._treePanel.root) {
                idsToRoot.push(currentNode.id);
                currentNode = currentNode.parentNode;
            }

            // Walk ids from root -> selected making data structure for trail
            var trail = [];
            for (var i = idsToRoot.length - 1; i >= 0; i--) {
                var node = this._treePanel.nodeHash[idsToRoot[i]];
                var pr = this.getPersistentReference(node.id);
                trail.push({ name: node.text, persistentReference: pr, hash: pr.getHash() });
            }

            return trail;
        },

        //
        // Select node in tree
        //
        selectNode: function(nodeId) {
            var node = this._treePanel.nodeHash[nodeId];

            if (node != null) {
                // Node is currently in tree, so select it.
                node.ensureVisible(function() {
                    node.select();
                });
            }
            else {
                // Node is not currently in the tree, get toc entries up to node from server
                var nodeIdParts = Ait.Aspect.TocNodeId.parse(nodeId);
                Ait.Aspect.Services.Toc.getNestedToNodeId(nodeIdParts.id, nodeIdParts.type, function(tocEntries) { this._getFromRoot_received(tocEntries, nodeId); }, this);
            }
        },

        //
        // Refresh current level of tree
        //
        refresh: function(pageId, bookTocId, tocType) {
            var root = this._treePanel.getRootNode();

            if (bookTocId == 0) {
                root.reload();
                return;
            }

            for (var i = root.childNodes.length - 1; i >= 0; i--)
                root.removeChild(root.childNodes[i]);

            var fnCallback = function(tocEntries) {
                if (!tocEntries) throw "Table-of-contents entries were not received from the server. Please refresh the page.";
                this._getFromRoot_received(tocEntries, Ait.Aspect.TocNodeId.get(bookTocId, tocType));
            }
            Ait.Aspect.Services.Toc.getNestedToNodeId("" + bookTocId, tocType, fnCallback, this);
        },

        _getFromRoot_received: function(tocEntries, selectNodeId) {

            // Update the tree with the received toc entries.
            this._treePanel.getLoader().loadWithChildren(this._treePanel.root, tocEntries);

            // Select node & make visible
            var selectNode = this._treePanel.nodeHash[selectNodeId];
            if (!selectNode)
                this._treePanel.root.firstChild.select(); //select the root element
            else {
                selectNode.ensureVisible();
                selectNode.select();
            }
        },
        //
        // Clear any selected node
        //
        clearSelection: function() {
            this._treePanel.selModel.clearSelections();
        },
        
        getSelectedLibraryIds: function() {
            
            var node = this._treePanel.selModel.selNode;
            if(!node) return null;

            return {
                libraryBookId: this._getAitBookId(node) || 0,
                libraryTopicId: (node.attributes.libraryTopicId) ? node.attributes.libraryTopicId : 0
            };
        },
        
        getSelectedTreeNodeId: function() {
            if(!this._treePanel) return null;
            var node = this._treePanel.selModel.selNode;
            if(!node) return null;
            return node.id;
        },
        
        getNodeInfo: function(treeNodeId) {
            var node = this._treePanel.nodeHash[treeNodeId];
            if(!node || !node.attributes) return null;
        
            return {
                text: node.attributes.text || ""
            }
        },


        //
        // Persistent Reference handling code.  
        //Possible this would be just as useful in a different file.
        //
        getPersistentReference: function(treeNodeId) {

            //find node
            var node = this._treePanel.nodeHash[treeNodeId];
            if (!node) throw new Error("Invalid node passed to getPersistentReference"); //in theory this node was just passed to us by the navigation tree so if its invalid something gone badly wrong.

            var bookTocId = Ait.Aspect.TocNodeId.parse(treeNodeId).id;
            var libraryBookId = this._getAitBookId(node);
            var isBookRootNode = node.attributes.libraryBookId && !node.attributes.libraryTopicId; //signature for a book-root node

            var persistentReference = new Ait.Aspect.PersistentReference();
            
            if (!libraryBookId) //then we assume it is a site node (above the book level)
                persistentReference.siteId = bookTocId;
            else {
                persistentReference.libraryBookId = libraryBookId;
                persistentReference.nodeId = bookTocId;
            
                if(!isBookRootNode)
                    persistentReference.libraryTopicId = node.attributes.libraryTopicId;
            }

            return persistentReference;
        },

        getNodeIdFromPersistentReference: function(persistentReference, callback) {

            if (!persistentReference) {
                callback(null);
                return;
            }

            if (persistentReference.siteId) {
                callback(Ait.Aspect.TocNodeId.get(persistentReference.siteId, Ait.Aspect.TocEntryType.Site));
                return;
            }

            //check if node id valid - it may be out of date
            if (persistentReference.nodeId) {

                var treeNodeType = (persistentReference.libraryTopicId) ? Ait.Aspect.TocEntryType.Book : Ait.Aspect.TocEntryType.Site;
                var treeNodeId = Ait.Aspect.TocNodeId.get(persistentReference.nodeId, treeNodeType);
                var node = this._treePanel.nodeHash[treeNodeId];

                if (node) {
                    //check if this node still applies to the book or topic we are expecting it to
                    if ((node.attributes.libraryBookId && node.attributes.libraryBookId == persistentReference.libraryBookId)
                    || (node.attributes.libraryTopicId && node.attributes.libraryTopicId == persistentReference.libraryTopicId)) {

                        callback(treeNodeId);
                        return;
                    }
                }
            }

            //traverse entire tree, filtering by book (or stopping there if no topic) to find topic
            var nodes = [];
            var findNodesRecursively = function(parentNode) {
                parentNode.eachChild(function(node) {

                    if (node.attributes.libraryBookId) {
                        //if there is only a book and we have found it then add the node
                        if (!persistentReference.libraryTopicId && node.attributes.libraryBookId == persistentReference.libraryBookId)
                            nodes.push(node);

                        //if we are searching for a topic, and it is not in this book then don't bothering looking thru child nodes
                        if (persistentReference.libraryTopicId && node.attributes.libraryBookId != persistentReference.libraryBookId)
                            return;
                    }

                    //have we reached a matching topic?
                    if (node.attributes.libraryTopicId && node.attributes.libraryTopicId == persistentReference.libraryTopicId)
                        nodes.push(node);

                    findNodesRecursively(node);
                });
            };

            findNodesRecursively(this._treePanel.getRootNode());

            //stop if matching node was found
            if (nodes.length > 0) {
                callback(nodes[0].id);
                return;
            }

            //node not found, so we need to get information from the server that will enable it to be looked up
            Ait.Aspect.Services.Toc.libraryCodeToTocEntryID(persistentReference.libraryBookId, persistentReference.libraryTopicId,
            function(resp) {
                if (resp.length && resp.length == 2)
                    callback(Ait.Aspect.TocNodeId.get(resp[0], resp[1]));
                else
                    callback(null);
            }, this, callback, this);

        },

        //
        // Navigate up until site parent is found with book id in it.
        //
        _getAitBookId: function(node) {

            while (node != null) {
                if (node.attributes.libraryBookId)
                    return node.attributes.libraryBookId;

                node = node.parentNode;
            }
            return null;
        }
    });
} ();
Ext.namespace('Ait.Aspect');

Ait.Aspect.ContentPanel = {
    _nsConfig: Ait.Aspect.EngineWeb2.Config,
    _elem: null, _loadMask: null,

    _breadcrumbTemplate: new Ext.Template(
        '<div class="breadcrumbItem">',
            '<a href="#{hash}">{name}</a>',
            '<img class="breadcrumbSplitter" src="{blankImgUrl}" /> ',
        '</div>'
    ),

    _breadcrumbCurrentTemplate: new Ext.Template(
        '<div class="currentBreadCrumbItem">',
            '<b>{name}</b>',
        '</div>'
    ),

    _feedbackEmailLinkTemplate: new Ext.Template(
        '<div class="feedbackEmail">',
            '<a onclick="Ait.Aspect.ContentPanel.submitFeedback(); return false;" href="#">',
                '<img class="feedbackIcon" src="{blankImgUrl}" align="top" />',
                ' Submit Feedback',
            '</a>',
        '</div>'
    ),

    _pageEmailLinkTemplate: new Ext.Template(
        '<div class="pageEmail">',
            '<a onclick="Ait.Aspect.ContentPanel.emailPage(); return false;" href="#" >',
                '<img class="emailIcon" src="{blankImgUrl}" align="top" />',
                ' Email Page',
            '</a>',
        '</div>'
    ),

    _editInAITLinkTemplate: new Ext.Template(
        '<div class="editInAit">',
            '<a onclick="Ait.Aspect.ContentPanel.editInAIT(); return false;" href="#" >',
                '<img class="editInAitIcon" src="{blankImgUrl}" align="top" />',
                ' Edit in Author-it',
            '</a>',
        '</div>'
    ),

    _editInLiveLinkTemplate: new Ext.Template(
        '<div class="editInLive">',
            '<a onclick="Ait.Aspect.ContentPanel.editInLive(); return false;" href="#" >',
                '<img class="editInLiveIcon" src="{blankImgUrl}" align="top" />',
                ' Edit in Author-it Live!',
            '</a>',
        '</div>'
    ),

    _page: null,
    _pageTitle: '',

    _feedbackEmail: null,
    _pageEmail: null,

    //
    // Initialize the content panel
    //
    init: function(config) {

        this._contentHeadPanel = new Ext.Panel({
            contentEl: 'contentHead',
            border: false,
            region: 'north'
        });

        this._contentHeadPanel.on('resize', function() {
            this._contentHeadPanel.suspendEvents();
            this._exposeContentHeader();
            this._contentHeadPanel.resumeEvents();
        }, this);

        this._contentPanel = new Ext.Panel({
            contentEl: 'contentPanel',
            margin: '4 3 0 0',
            border: false,
            region: 'center',
            autoScroll: true
        });

        this._panel = new Ext.Panel({
            region: 'center',
            layout: 'border',
            items: [this._contentHeadPanel, this._contentPanel]
        });

        this._panel.setVisible(false);

        this._feedbackEmail = {
            toAddress: config.feedbackEmail.toAddress,
            subjectTemplate: new Ext.Template(config.feedbackEmail.subject),
            bodyTemplate: new Ext.Template(config.feedbackEmail.body)
        };

        this._pageEmail = {
            subjectTemplate: new Ext.Template(config.pageEmail.subject),
            bodyTemplate: new Ext.Template(config.pageEmail.body)
        };

        this._editLinks = config.editLinks;
    },

    _exposeContentHeader: function() {
        var headerHeight = Ext.get('contentHead').getHeight();
        this._contentHeadPanel.setHeight(headerHeight);
        this._panel.doLayout();
    },

    //
    // Get panel
    //
    getPanel: function() {
        return this._panel;
    },

    //
    // Display a page
    //
    getCSS: function(page, onReady) {

        var cssHref = Ait.Aspect.Services.ASPECT_BASE_URL + 'BookCss.ashx?id=' + page.BookId + "&v=" + Ait.Aspect.Viewer.currentVersion;

        var existingNodes = Ext.select("link[href=" + cssHref + "]");
        if (existingNodes.getCount() == 0)
            YAHOO.util.Get.css(cssHref, { onSuccess: onReady, onFailure: onReady });
        else
            onReady();
    },

    displayPage: function(page, navigationTrail) {
        this._renderBreadcrumbTrail(navigationTrail);
        this._renderLinks();

        Ext.get('pageContent').update(page.Html);
        // Update print stuff
        Ext.get('pageContentPrint').update(page.Html);
        Ext.get('pageTitlePrint').update(navigationTrail[navigationTrail.length - 1].name);
        this._pageTitle = navigationTrail[navigationTrail.length - 1].name;
        this._page = page;

        this._enableLinks('pageContent');
        this._setFileObjectSrc();
        this._exposeContentHeader();

        var cssHref = Ait.Aspect.Services.ASPECT_BASE_URL + 'BookCss.ashx?id=' + page.BookId + "&v=" + Ait.Aspect.Viewer.currentVersion;
        var cssNodes = Ext.select("link[href=" + cssHref + "]");
        if (cssNodes.getCount() > 0)
            this._IE7BackgroundThroughFloatsFix(cssNodes.first().dom, Ext.get('pageContent'));
    },

    //
    // Scroll to very top of page
    //
    scrollToTop: function() {
        Ext.get(this._panel.el.dom.firstChild.firstChild).scrollTo('top', 0, true);
    },

    //
    // Scroll to a topic in the currently displayed page.
    //
    scrollToTopic: function(topicId) {
        // If topic is first on page, scroll to very top
        if (this._page.LibraryTopicIds != null && this._page.LibraryTopicIds[0] == topicId)
            this.scrollToTop();
        else {
            var topicAnchor = Ext.get("topicAnchor_" + topicId);

            if (topicAnchor != null)
                Ait.Aspect.DomUtil.scrollToTop(topicAnchor, this._panel.el.dom.firstChild.firstChild);
        }
    },

    //
    // Clear page contents and display navigation trail
    // (site toc entries have no page associated).
    //
    displaySitePage: function(navigationTrail) {
        this._renderBreadcrumbTrail(navigationTrail);
        Ext.get('pageContent').update("");
    },

    //
    // Render the breadcrumb trail
    //
    _renderBreadcrumbTrail: function(navigationTrail) {
        if (!this._nsConfig.UI.ShowBreadCrumbs) return;

        var trailHtml = '';
        for (var i = 0; i < navigationTrail.length - 1; i++) {
            var trailItem = navigationTrail[i];
            trailItem.blankImgUrl = Ext.BLANK_IMAGE_URL;
            trailHtml += this._breadcrumbTemplate.apply(trailItem);
        }

        var leaf = navigationTrail[navigationTrail.length - 1];
        var lastTrailItemName = leaf.name;

        //'this._nsConfig.UI.EditMode' is related to Authorit Aspect SharePoint integration
        if (this._nsConfig.UI.EditMode) {
            //add treeNodeId 'Content Id' to the name (temporary) to be used for 'Home Page Content Id' property setting in Authorit Aspect SharePoint webpart
            navigationTrail[navigationTrail.length - 1].name = lastTrailItemName +
                "<span style='font-weight: normal; font-style: italic;'> - Content Id: " + leaf.hash + "</span>";
        }
        trailHtml += this._breadcrumbCurrentTemplate.apply(navigationTrail[navigationTrail.length - 1]);

        //set the name back to what it was
        navigationTrail[navigationTrail.length - 1].name = lastTrailItemName;
        Ext.get('breadcrumbTrail').update(trailHtml + '<div style="clear: both"></div>');
    },

    //
    // Render header links
    //
    _renderLinks: function() {
        Ext.get('contentLinks').update([
            this._nsConfig.UI.ShowEmailPageLink ? this._pageEmailLinkTemplate.apply({ blankImgUrl: Ext.BLANK_IMAGE_URL }) : "",
            this._nsConfig.UI.ShowSubmitFeedbackLink ? this._feedbackEmailLinkTemplate.apply({ blankImgUrl: Ext.BLANK_IMAGE_URL }) : "",
            this._editLinks && this._editLinks.ait ? this._editInAITLinkTemplate.apply({ blankImgUrl: Ext.BLANK_IMAGE_URL }) : "",
            this._editLinks && this._editLinks.live && Ext.isIE ? this._editInLiveLinkTemplate.apply({ blankImgUrl: Ext.BLANK_IMAGE_URL }) : "",
            '<div style="clear: both"></div>'
        ].join(''));
    },

    //
    // Update 'a' tags, converting pageId, topicId, and bookTocId into an on-click event call to navigate
    //
    _enableLinks: function(id) {

        var links = Ext.get(id).dom.getElementsByTagName("a");
        for (var i = 0, ci = links.length; i < ci; i++) {
            var link = links[i];
            //Identify if its internal link should have pageId
            var topicId = link.getAttribute("topicId");

            // Apply Action/Behaviour to the links according to how LinkAction configured in UIConfig 
            switch (this._nsConfig.UI.LinkAction) {
                case this._nsConfig.LinkAction.Default:
                    Ait.Aspect.LinkFactory.Create(link); //No action taken (leave links behaviour as published)
                    break;
                case this._nsConfig.LinkAction.Disable:
                    link.removeAttribute("href"); //links are rendered as plane text
                    break;
                case this._nsConfig.LinkAction.Internal:
                    if (topicId)//Change action only if Internel link
                        link.setAttribute("hyperlinkType", "1"); //links navigate to the same window
                    Ait.Aspect.LinkFactory.Create(link);
                    break;
                case this._nsConfig.LinkAction.External:
                    if (topicId)//Change action only if Internel link
                        link.setAttribute("hyperlinkType", "2"); //links navigate to new window
                    Ait.Aspect.LinkFactory.Create(link);
                    break;
            }
        }
    },


    //
    // Takes an element with a fileObjectId attribute and changes it to point to FileObject.ashx
    // This is called by the generated iframes as well as within ContentPanel.
    //
    convertFileObjToAttribute: function(eles, attr) {
        for (var i = 0, ci = eles.length; i < ci; i++) {
            var ele = eles[i];
            var fileObjectId = ele.getAttribute("fileObjectId");
            if (!fileObjectId) continue;

            ele.setAttribute(attr, Ait.Aspect.Services.ASPECT_BASE_URL + "FileObject.ashx?id=" + fileObjectId);
        }
    },

    //
    // Update 'img' tags, converting fileObjectId attribute into src
    //
    _setFileObjectSrc: function() {

        this.convertFileObjToAttribute(Ext.get('pageContent').dom.getElementsByTagName("img"), "src");
        this.convertFileObjToAttribute(Ext.get('pageContentPrint').dom.getElementsByTagName("img"), "src");

        var iframes = Ext.get('pageContent').dom.getElementsByTagName("iframe");
        this.convertFileObjToAttribute(iframes, "src");
        this._prepareIFrames(iframes);

        iframes = Ext.get('pageContentPrint').dom.getElementsByTagName("iframe");
        this.convertFileObjToAttribute(iframes, "src");
        this._prepareIFrames(iframes);
    },

    _prepareIFrames: function(iframes) {
        for (var i = 0, ci = iframes.length; i < ci; i++)
            this._resizeIframeToFit(iframes[i]);
    },

    _resizeIframeToFit: function(iframe) {

        var existingLoad = iframe.contentWindow.onload;

        var isIframeLoaded = function() {
            return iframe.contentWindow &&
                    iframe.contentWindow.document &&
                    iframe.contentWindow.document.body &&
                    iframe.contentWindow.document.body.innerHTML;
        }

        var onload = function(isEvent) {

            //IE Only: wait for iframe to be loaded.
            if (iframe.readyState !== undefined && (!iframe.readyState == "complete" || !isIframeLoaded())) {
                onload.defer(100);
                return;
            }

            //run load event already present on the page
            if (isEvent !== false && existingLoad)
                existingLoad();

            //resize iframe
            var doc = iframe.contentWindow.document;
            var elIFrame = Ext.get(iframe);
            elIFrame.setHeight(doc.documentElement.scrollHeight);
            elIFrame.setWidth(doc.documentElement.scrollWidth);
        };

        if (iframe.onreadystatechange) //IE
            iframe.onreadystatechange = onload;
        else { //FF
            iframe.onload = onload;
            onload(false); //called here in case iframe loads before reaching this method (i.e. from cache).
        }
    },

    //
    // Email link to current page.
    //
    emailPage: function() {
        var templateVariables = this._getTemplateVariables();

        Ait.Aspect.MailUtil.mailto('',
            this._pageEmail.subjectTemplate.apply(templateVariables),
            this._pageEmail.bodyTemplate.apply(templateVariables)
        );
    },

    //
    // Submit feedback on current page.
    //
    submitFeedback: function() {
        var templateVariables = this._getTemplateVariables();

        var addresses = this._feedbackEmail.toAddress;

        if (this._page.AuthorEmails != null && this._page.AuthorEmails.length > 0)
            addresses = this._page.AuthorEmails.join('; ');

        Ait.Aspect.MailUtil.mailto(
            addresses,
            this._feedbackEmail.subjectTemplate.apply(templateVariables),
            this._feedbackEmail.bodyTemplate.apply(templateVariables)
        );
    },

    //
    // Open link in Author-it
    // 'Edit in Authorit' link format = "authorit: database_path /objectid{libraryTopicId} /bookid{libraryBookId}"
    //
    editInAIT: function() {
        if (!this._editLinks || !this._editLinks.ait || !this._page.LibraryTopicIds) return;
        var url = this._editLinks.ait.replace("{libraryTopicId}", this._getLibraryTopicId());
        url = url.replace("{libraryBookId}", this._page.LibraryBookId);
        window.open(url);
    },

    //
    // Open link in Author-it Live!
    // 'Edit in Author-it Live' link format = "http://server/?alias=libraryAlias&opentopic={libraryTopicId}&openbook={libraryBookId}"
    //
    editInLive: function() {
        if (!this._editLinks || !this._editLinks.live || !this._page.LibraryTopicIds) return;
        var url = this._editLinks.live.replace("{libraryTopicId}", this._getLibraryTopicId());
        url = url.replace("{libraryBookId}", this._page.LibraryBookId);
        window.open(url);
    },

    _getLibraryTopicId: function() {

        var selectedIds = Ait.Aspect.NavigationTree.getSelectedLibraryIds();

        if (selectedIds && selectedIds.libraryTopicId)
            return selectedIds.libraryTopicId;

        return this._page.LibraryTopicIds[0];
    },

    //
    // Get variables to apply template.
    // 
    _getTemplateVariables: function() {

        var pageUrl = window.location.href;
        var topicList = '' + this._page.LibraryTopicIds.join(', ');

        var templateVariables = {
            Title: this._pageTitle,
            UserName: Ait.Aspect.Viewer.user.UserName,
            PageURL: removeQueryStringsFromUrl(pageUrl, ["f", "shpoint"]),
            TopicList: topicList
        };

        this._addOptionalSharepointVariable(templateVariables, pageUrl);
        return templateVariables;
    },

    _addOptionalSharepointVariable: function(templateVariables, pageUrl) {

        var sharepointPageURL = getQueryStringValueFromUrl(window.location.href, "shpoint");
        if (!sharepointPageURL) return;

        //If Aspect contained in another application (e.g. SharePoint)
        //make link point to parent page, and add a query string parameter to pass aspectcontent Id
        var aspectContentId = "";
        var hashPos = pageUrl.indexOf("#");
        if (hashPos > 0) //Extract aspect Content Id
            aspectContentId = pageUrl.substring(hashPos + 1);

        if (!aspectContentId) return;

        sharepointPageURL = removeQueryStringsFromUrl(sharepointPageURL, ["content"]);
        var contentIdQueryString = "content=" + aspectContentId;

        if (sharepointPageURL.indexOf("?") > 0) //Check for QueryString
            sharepointPageURL += "&" + contentIdQueryString;
        else
            sharepointPageURL += "?" + contentIdQueryString;

        templateVariables.SharepointPageURL = sharepointPageURL;
    },

    //
    // This method was added in response to bug 32638, which is tracable to a bug in IE7
    // where the background of a paragraph under a float leaked through the float.
    //
    // Unfortunately it cannot be resolved at the renderer as we don't cache based on browser type,
    // and this means we have to analyze the stylesheets received for the problematic pattern, and resolve
    // it there.
    // 
    // Info on the bug available at:
    // http://www.mikepadgett.com/technology/technical/ie7-only-float-bug-on-elements-with-italics-and-background-properties/
    // http://muffinresearch.co.uk/archives/2006/12/28/bug-ie7-absolutely-positioned-italics/ (comment 19)
    //
    _IE7BackgroundThroughFloatsFix: function(cssLinkEle, extEle) {
        if (!Ext.isIE7) return;

        //Looking for paragraphs whose class has a background color set, and a width of 100%
        var paras = extEle.select('p');
        paras.each(function(para) {
            var className = para.dom.className;
            if (!className) return;

            var rule = Ait.Aspect.GetClassFromStylesheet(cssLinkEle.href, "." + className);
            if (!rule) return;

            //does the style from this rule match the case where the rendering bug appears?
            var style = rule.style;
            if (!style.backgroundColor || style.width != "100%") return;

            //update the element's style to override the class setting:
            para.dom.style.width = "auto";

        });
    }
}

Ext.namespace('Ait.Aspect');

Ait.Aspect.SearchDirection = { none: 0, asc: 1, desc: 2 };
Ait.Aspect.SortField = { none: 0, topictitle: 1, relevance: 2 }

Ait.Aspect.SearchResultsPanel = function() {
    return Ext.apply(new Ext.util.Observable(), {
        _relevanceTemplate: new Ext.Template(
            '<table class="searchRelevance" align="center">',
            '<tr>',
                '<td>',
                    '<div class="searchRelevance" style="width: {relevanceTotalWidth}px;">',
                    '<div class="searchRelevanceBar" style="width: {relevanceWidth}px;"></div></div>',
                    '<div class="searchRelevanceText" style="color: {relevanceTextColor};">{Relevance}%</div>',
                '</td>',
            '</tr>',
            '</table>'
        ),

        //
        // Initialize the search results panel
        //
        init: function(config) {
            this.addEvents({
                search: true,
                search_complete: true,
                result_selected: true
            });

            this._books = {};
            if (config.books)
                this._initalizeBooks(config.books);

            this._searchResults = null;

            Ext.get("searchResultsGrid").parent().setStyle("vertical-align", "top")
            Ext.get("searchResultsFilter").parent().setStyle("vertical-align", "top")

            // Create filter panel
            this._filterPanel = new Ait.Aspect.CheckedFilterPanel({
                title: 'Change the books included in the search results:',
                renderTo: 'searchResultsFilter',
                style: 'padding-left: 10px',
                bodyStyle: 'font-size: 12px; padding: 5px',
                updateButtonText: 'Update Search Results'
            });

            this._filterPanel.on("update", this._updateBookIds, this);
            this._initalizeFilterPanel();

            // Create panel
            this._panel = new Ext.Panel({
                region: 'center',
                contentEl: 'searchResultsPanel',
                margins: '4 3 0 0',
                autoScroll: true,
                containerScroll: true
            });

            // Create results data store (for grid)
            this._resultsStore = new Ext.data.JsonStore({
                data: { Results: [], TotalCount: 0 },
                fields: [
                   { name: 'PageId', type: 'int' },
                   { name: 'TopicId', type: 'int' },
                   { name: 'BookTocId', type: 'int' },
                   { name: 'BookId', type: 'int' },
                   { name: 'TopicTitle' },
                   { name: 'Excerpt' },
                   { name: 'Relevance' },
                   { name: 'relevanceWidth', type: 'int' },
                   { name: 'relevanceTotalWidth', type: 'int' },
                   { name: 'relevanceTextColor' }
                ],
                root: 'Results',
                totalProperty: 'TotalCount',
                remoteSort: true
            });
            this._resultsStore.setDefaultSort('Relevance', 'desc');
            this._resultsStore.on("load", this._searchComplete, this);
            this._resultsStore.on("loadexception", this._searchComplete, this);

            this._resultsStore.proxy = new Ait.Web.JsonService.Proxy({ url: Ait.Aspect.Services.ASPECT_BASE_URL + 'Services/Search.asmx/SearchContent' });
            this._resultsStore.proxy.on('beforeload', function(_, params) {

                Ext.apply(this._lastParams, {
                    start: params["start"],
                    limit: params["limit"]
                });

                params.searchOptions = {
                    Query: this._lastParams.text,
                    BookIds: this._lastParams.bookIds,
                    Start: this._lastParams.start,
                    Limit: this._lastParams.limit,
                    SortField: this._sortFieldToEnum(params["sort"]),
                    SortDirection: this._sortDirectionToEnum(params["dir"])
                }

            }, this);
            this._resultsStore.proxy.on("beforeLoadResponse", function(o, success, response) {
                //response being returned is in the incorrect format, but the .Value field is the correct format:
                var json = Ext.decode(response.responseText);
                response.responseText = Ext.encode(json.Value);
            }, this);

            var pagingBar = new Ext.PagingToolbar({
                pageSize: Ait.Aspect.SearchQuery.DEFAULT_LIMIT,
                store: this._resultsStore,
                displayInfo: true,
                displayMsg: 'Displaying results {0} - {1} of {2}',
                emptyMsg: "No results found"
            });

            this._resultsHeading = Ext.get('searchResultsHeading');

            // Create the Grid
            this._resultsGrid = new Ext.grid.GridPanel({
                store: this._resultsStore,
                columns: [
                    { header: 'Book', dataIndex: 'BookId', width: 40, renderer: this._renderBook.createDelegate(this) },
                    { header: 'Topic', dataIndex: 'TopicTitle', width: 60, sortable: true, renderer: this._renderTopic.createDelegate(this) },
                    { header: 'Relevance', dataIndex: 'Relevance', width: 30, sortable: true, align: 'center', renderer: this._renderRelevance.createDelegate(this) }
                ],
                viewConfig: {
                    forceFit: true,
                    enableRowBody: true,
                    showPreview: true,
                    getRowClass: this._renderRowBody.createDelegate(this),
                    emptyText: 'No results returned.'
                },

                autoHeight: true,
                autoExpandColumn: 'Status',
                enableHdMenu: false,
                enableColumnMove: false,
                stripeRows: false,
                frame: true,
                width: 600,
                cls: 'grayPanel',
                tbar: pagingBar
            });
            this._resultsGrid.render('searchResultsGrid');

            this._resultsGrid.on('rowclick', this._resultsGrid_rowclick, this);
        },

        //
        // Initalize books index
        //
        _initalizeBooks: function(books) {
            this._books = {};
            for (var i = 0, ci = books.length; i < ci; i++) {
                var book = books[i];
                this._books[book.BookId] = book.Title;
            }
        },

        //
        // Render row body
        //
        _renderRowBody: function(record, rowIndex, p, store) {
            p.body = '<p class="searchExcerpt">' + record.data.Excerpt + '</p>';
            return '';
        },

        //
        // Render relevance
        //
        _renderRelevance: function(value, metadata, record, rowIndex, colIndex, store) {

            // Calcualte widths for relevance bar
            var relevanceTotalWidth = 60;

            // Round relevance to an integer
            var relevance = parseInt(record.get("Relevance").toFixed());

            return this._relevanceTemplate.apply({
                Relevance: relevance,
                relevanceWidth: relevance / 100 * relevanceTotalWidth,
                relevanceTotalWidth: relevanceTotalWidth,
                relevanceTextColor: relevance < 65 ? '#000' : '#fff'
            });
        },

        // Render book
        //
        _renderBook: function(value) {
            var title = this._books[value] || "";
            return '<div class="searchTitle">' + title + '</div>';
        },

        //
        // Render topic
        //
        _renderTopic: function(value) {
            return '<div class="searchTitle">' + value + '</div>';
        },

        //
        // Results row clicked
        //
        _resultsGrid_rowclick: function(grid, rowIndex, e) {
            var record = this._resultsStore.getAt(rowIndex);
            this.fireEvent('result_selected', {
                pageId: record.get('PageId'),
                topicId: record.get('TopicId'),
                bookTocId: record.get('BookTocId')
            });
        },

        _sortFieldToEnum: function(name) {
            if (!name) return Ait.Aspect.SortField.none;

            name = name.toLowerCase();
            if (name == "topictitle") return Ait.Aspect.SortField.topictitle;
            if (name == "relevance") return Ait.Aspect.SortField.relevance;

            return Ait.Aspect.SortField.none;
        },

        _sortDirectionToEnum: function(dir) {
            if (!dir) return Ait.Aspect.SearchDirection.none;

            dir = dir.toLowerCase();
            if (dir == "asc") return Ait.Aspect.SearchDirection.asc;
            if (dir == "desc") return Ait.Aspect.SearchDirection.desc;

            return Ait.Aspect.SearchDirection.none;
        },

        //
        // Add all the books in the site to the filter-panel
        //
        _initalizeFilterPanel: function() {

            // this._books[book.BookId] = book.Title;
            var books = [];
            for (var bookId in this._books) {
                var title = this._books[bookId];
                var value = parseInt(bookId);
                if(isNaN(value)) continue;
                
                books.push({ name: title, value: value});
            }
            books.sort(function(a, b) {
                return a.name.toLowerCase() > b.name.toLowerCase(); //case-insensitive sort
            });

            this._filterPanel.newFilter(books);
        },

        //
        // Get the currently selected books.
        //
        // This has been made public so it can be accessed from the HeaderToolbar. This is an indication
        // that the search book filter probably shouldn't be coupled to the search results panel.
        //
        getSelectedBookIds: function() {

            if (!this._filterPanel) return null;

            var filterResults = this._filterPanel.getFilter();
            if (!filterResults) return null;

            var bookIds = [];
            for (var i = 0, ci = filterResults.length; i < ci; i++)
                bookIds.push(filterResults[i].value);

            return bookIds;
        },

        //
        // Get panel
        //
        getPanel: function() {
            return this._panel;
        },

        //
        // Repeat last search
        //
        searchAgain: function() {
            if (!this._lastParams || !this._lastParams.text) return;
            this.search(this._lastParams, true);
        },


        _updateBookIds: function() {
            this.search(Ext.apply(this._lastParams, {
                bookIds: this.getSelectedBookIds()
            }), true);
        },

        _searchComplete: function() {

            // Output heading
            var totalResults = this._resultsStore.getTotalCount();
            var headerString = String.format('Search for {0} returned {1} results:', this._lastParams.text, totalResults > 0 ? totalResults : 'no');
            this._resultsHeading.update(headerString);

            // Update printable data
            Ext.get('pageTitlePrint').update("Search Results");
            Ext.get('pageContentPrint').update(this._resultsHeading.dom.innerHTML + Ext.get('searchResultsGrid').dom.innerHTML);

            this.fireEvent('search_complete', this._lastParams);
        },
        
        //
        // Perform search
        // Returns false is search is not being performed.
        //
        search: function(searchQuery, force) {

            if (force !== true && searchQuery.equals(this._lastParams))
                return false;
                
            //check book ids are valid and sync UI to reflect query
            this._filterPanel.setSelected(searchQuery.bookIds);
            if (!searchQuery.bookIds || searchQuery.bookIds.length == 0) {
                Ext.MessageBox.alert("Error", "At least one book must be selected before searching.");
                return false;
            }

            this.fireEvent('search');
            this._lastParams = searchQuery;
            this._resultsStore.load({ params: this._lastParams });
            return true;
        }
    });
} ();
Ext.namespace('Ait.Aspect');

//
//
// Events:
//  filter_change - fires whenever the filter changes
//  update - fires when the user clicks the update button
//
Ait.Aspect.CheckedFilterPanel = function(config) {

    config.bbar  = [{
        text: config.updateButtonText || 'Update',
        handler: this._update,
        scope: this    
    },'-',{
        text: 'Select All',
        handler: this._selectAll,
        scope: this
    },{
        text: 'Select None',
        handler: this._selectNone,
        scope: this
    }];
    
    Ait.Aspect.CheckedFilterPanel.superclass.constructor.call(this, config);
};

Ext.extend(Ait.Aspect.CheckedFilterPanel, Ext.Panel, {       

    //
    //
    //
    getFilter: function() {
        var filterSelection = [];
        
        this._eachCheck(function(checkItem) {
            if(checkItem.checked)
                filterSelection.push({ name: checkItem.name, value: checkItem.initialConfig.dataValue });
        })

        return filterSelection;
    },
    
    //
    // Update the items selected in the panel.
    //
    setSelected: function(values, supressEvents) {
    
        this._eachCheck(function(checkItem) {
            checkItem.un("check", this._fireFilterChange, this);
            
            //indexOf is an Ext extension to the built-in Array class.
            var checked = values.indexOf(checkItem.initialConfig.dataValue) >= 0;
            checkItem.setValue(checked);
            
            checkItem.on("check", this._fireFilterChange, this);
        });
        
        if(supressEvents !== true)
            this._fireFilterChange();
    },


    //
    // Replace the items in the filter
    // filterData: [{name:, value:}, ...]
    //
    newFilter: function(filterData) {
        this._renderItems(filterData);
        this._fireFilterChange();
    },

    //
    // Tell owner to reload whatever the filter data refers to.
    //
    _update: function() {
        this.fireEvent('update');
    },
    
    //
    // Run the passed-in function on each checkbox
    //
    _eachCheck: function(fn) {
        if(!this.items) return;
        for(var i = this.items.length - 1; i >= 0; i--) {
            var item = this.items.get(i);
            if(item.getXType() != "checkbox") continue;
            
            fn.call(this, item);       
        }
    },
    
    //
    // Notify any listeners that the check-state of the filter has changes
    //
    _fireFilterChange: function() {
        this.fireEvent('filter_change');
    },
    
    //
    // Select all checks, firing the filter_change event only once
    //
    _selectAll: function() {
    
        this._eachCheck(function(checkItem) {
            checkItem.un("check", this._fireFilterChange, this);
            checkItem.setValue(true);
            checkItem.on("check", this._fireFilterChange, this);
        });
        
        this._fireFilterChange();
    },
    
    //
    // Unselect all checks, firing the filter_change event only once
    //
    _selectNone: function() {
    
        this._eachCheck(function(checkItem) {
            checkItem.un("check", this._fireFilterChange, this);
            checkItem.setValue(false);
            checkItem.on("check", this._fireFilterChange, this);
        });
        
        this._fireFilterChange();
    },
    
    //
    // Draw the filter-items named in the arguments as checkboxes
    //
    _renderItems: function(filterData) {

        //remove all existing check boxes    
        this._eachCheck(function(checkItem) {
            this.remove(checkItem);
        });
    
        //add new checks
        for(var i = 0, ci = filterData.length; i < ci; i++) {
            this.add(new Ext.form.Checkbox({ 
                name: filterData[i].name, 
                boxLabel: filterData[i].name,  
                checked: true,
                style: 'margin-left: 10px',
                dataValue: filterData[i].value
            }));
        }
        
        this.doLayout();
        
        //setup events
        this._eachCheck(function(checkItem) {
            checkItem.on("check", this._fireFilterChange, this);
        })
    }
});   
Ext.namespace('Ait.Aspect');

//
// User options dialog.
//
Ait.Aspect.OptionsDialog = function() {
     return Ext.apply(new Ext.util.Observable(), {

        _user: null, _filterCriteria: null,
        
        _window: null, _formItems: [],
        
        WINDOW_DEFAULT_WIDTH: 300, FORM_FIELD_WIDTH: 150, FORM_LABEL_WIDTH: 100, PADDING_WIDTH: 40,
        

        //
        // Init user options dialog
        //
        init: function(config) {
            this._user = config.user;
            this._filterCriteria = config.filterCriteria;
            
            this.addEvents({
                options_saved: true
            });
            
            // If there are no filter criteria, dialog is not used
            if(!this._filterCriteria || this._filterCriteria.length == 0)
                return;
            
            var formItems = [];
            
            for(var i = 0; i < this._filterCriteria.length; i++) {
                var filterCriteria = this._filterCriteria[i];
                var userProperty = this._findUserProperty(filterCriteria);
   
                // Make appropriate form input
                if(!filterCriteria.IsEditable || filterCriteria.VariableType == Ait.Aspect.VariableType.Normal) {
                    // Normal variables get text boxes
                    var textField = new Ext.form.TextField({
                        fieldLabel: filterCriteria.DisplayName || filterCriteria.UserPropertyName,
                        name: 'property_' + filterCriteria.FilterCriteriaId,
                        value: userProperty != null && userProperty.Value.length > 0 ? userProperty.Value : "(no value set)",
                        filterCriteria: filterCriteria
                    });
                    
                    if(!filterCriteria.IsEditable) {
                        textField.readOnly = true;
                        textField.cls = "readonly";
                        textField.on("focus", function() { textField.getEl().blur();}); //stop the cursor appearing
                    }
                    
                    this._formItems.push(textField);
                }
                else if(filterCriteria.VariableType == Ait.Aspect.VariableType.ListOfValues) {
                    // List of values get list boxes
                    var data = [['']]; //start with the 'no value' option
                    
                    for(var j = 0; j < filterCriteria.VariableValues.length; j++)
                        data.push([filterCriteria.VariableValues[j]]);
                
                    var store = new Ext.data.SimpleStore({
                        fields: ['value'],
                        data: data
                    });

                    var selectBox = new Ext.ux.SelectBox({
                        tpl: '<tpl for="."><div class="x-combo-list-item" style="min-height: 1.2em;">{value}</div></tpl>',
                        fieldLabel: filterCriteria.DisplayName || filterCriteria.UserPropertyName,
                        valueField: 'value',
                        name: 'property_' + filterCriteria.FilterCriteriaId,
                        displayField: 'value',
                        store: store,
                        mode: 'local',
                        filterCriteria: filterCriteria
                    });
                    
                    if(userProperty != null)
                        selectBox.setValue(userProperty.Value);
                
                    this._formItems.push(selectBox);
                }
            }
            
            this._window = new Ext.Window({
                layout: 'fit',
                width: this.WINDOW_DEFAULT_WIDTH,
                autoHeight: true,
                closeAction: 'hide',
                plain: true,
                modal: true,
                title: 'User Options',
                resizable: false,
                defaultButton: 0,
                shadow: false,
                
                items: new Ext.FormPanel({
                    bodyStyle:'padding: 5px',
                    autoHeight: true,
                    defaults: { width: this.FORM_FIELD_WIDTH },
                    defaultType: 'textfield',
                    items: this._formItems
                }),

                buttons: [{
                    text: 'OK',
                    handler: this._ok,
                    scope: this
                }, {
                    text: 'Cancel',
                    handler: this._cancel,
                    scope: this
                }]
            });
        },

        
        //
        // Find user property value for this filter criteria (if there is one)
        //
        _findUserProperty: function(filterCriteria) {

            for(var j = 0; j < this._user.Properties.length; j++) {
                var userProperty = this._user.Properties[j];
                if(userProperty.PropertyId == filterCriteria.UserPropertyId)
                    return userProperty;
            }
            return null;
        },

        //
        // Show user options dialog
        //
        show: function() {        
            this._window.show();
            
            this._valuesAtShow = [];
            for (var i = 0; i < this._formItems.length; i++)
                if(this._formItems[i].getValue)
                    this._valuesAtShow.push( this._formItems[i].getValue() );
            
            // Set window width appropriately
            // (labels can differ in size).
            
            // Get widest label
            var labels = Ext.DomQuery.select('label', this._window.getEl().dom);
            var labelMaxWidth = this.FORM_LABEL_WIDTH;
            
            for(var i = 0; i < labels.length; i++) {
                var width = Ext.get(labels[i]).getTextWidth();
                
                if(width > labelMaxWidth)
                    labelMaxWidth = width;
            }
            
            // Set all labels to widest label width
            for(var i = 0; i < labels.length; i++) {
                Ext.get(labels[i]).setWidth(labelMaxWidth);
            }           
            
            // Set window width
            this._window.setWidth(labelMaxWidth + this.FORM_FIELD_WIDTH + this.PADDING_WIDTH);
        },
        
        //
        // Cancel dialog
        //
        _cancel: function() {
            this._window.hide();
        },
        
        //
        // Close dialog window
        //
        _ok: function() {        
            // Get user property values
            var userProperties = [];
            var isDirty = false;
                   
            for(var i = 0; i < this._formItems.length; i++) {
                var formItem = this._formItems[i];
                
                var isEditable = (formItem.initialConfig.filterCriteria.IsEditable && formItem.getValue);
                
                var value = null;
                if(isEditable)
                    value = formItem.getValue();
                else {
                    var userProperty = this._findUserProperty(formItem.initialConfig.filterCriteria);
                    if(userProperty)
                        value = userProperty.Value;
                }
                
                if(value == null) continue;
                
                userProperties.push({
                    PropertyId: formItem.initialConfig.filterCriteria.UserPropertyId,
                    Name: formItem.initialConfig.filterCriteria.UserPropertyName,
                    Value: value
                });
                    
                if(isEditable && this._valuesAtShow[i] !== formItem.getValue()) 
                    isDirty = true;
            }
            
            if(!isDirty) {
                // Nothing changed
                this._window.hide();
                return;
            }
        
            // Update user properties
            Ait.Aspect.Services.User.updateProperties(userProperties, false,
                function() {
                   this._window.hide();
                   this.fireEvent('options_saved');
                },
                this,
                function() {
                    Ext.MessageBox.alert('Error', 'Error saving user options.');
                },
                this);
        }

     });
}();
Ext.namespace("Ait.Aspect");

//
// Constructor
//
Ait.Aspect.PersistentReference = function(config){

    this.siteId = 0;
    this.libraryBookId = 0;
    this.libraryTopicId = 0;
    this.nodeId = 0;
    
    Ext.apply(this, config);
};

//
// Static Constructor
//
Ait.Aspect.PersistentReference.FromHash = function(hash) {
    var pr = new Ait.Aspect.PersistentReference();
    pr._parseHash(hash);
    return pr;
};

//
// Characters that may appear at the start of a hashed persistent reference 
// (we are not technically using 'n', but it is there in case we do)
//
Ait.Aspect.PersistentReference.HashPrefixMatch = function(ch) {
    if(!ch || typeof ch != "string") return false;
    return "abnt".indexOf(ch) >= 0;
}

Ait.Aspect.PersistentReference.prototype = {

    //
    // Build string representation of the object from its components
    //
    getHash: function() {
        
        if(this.siteId)
            return "a" + this.siteId;
        else if(this.libraryBookId && !this.libraryTopicId)
            return "b" + this.libraryBookId + "n" + this.nodeId;
        else if(!this.libraryBookId && this.libraryTopicId)
            return "t" + this.libraryTopicId + "n" + this.nodeId;
        else
            return "b" + this.libraryBookId + "t" + this.libraryTopicId + "n" + this.nodeId;
    },

    //
    // Set object properties from a string representation
    //
    _parseHash: function(hash) {

        if (!hash || hash.length < 2) return null;

        var states = { initial: 1, inSite: 2, inBook: 3, inTopic: 4, inNode: 5 },
        siteChars = [], bookChars = [], topicChars = [], nodeChars = [],
        reNum = /\d/,
        state = states.initial;

        for (var i = 0, ci = hash.length; i < ci; i++) {
            var ch = hash.charAt(i).toLowerCase();

            if (state == states.initial) {
                switch (ch) {
                    case 'a': state = states.inSite; break;
                    case 'b': state = states.inBook; break;
                    case 't': state = states.inTopic; break;
                    default: return null; //error - unexpected character
                }
            }

            else if (state == states.inSite) {
                if (reNum.test(ch)) siteChars.push(ch);
                else return null; //error - unexpected character
            }

            else if (state == states.inBook) {
                if (ch == 't') state = states.inTopic;
                else if (ch == 'n') state = states.inNode;
                else if (reNum.test(ch)) bookChars.push(ch);
                else return null; //error - unexpected character
            }

            else if (state == states.inTopic) {
                if (ch == 'n') state = states.inNode;
                else if (reNum.test(ch)) topicChars.push(ch);
                else return null; //error - unexpected character
            }

            else if (state == states.inNode) {
                if (reNum.test(ch)) nodeChars.push(ch);
                else return null; //error - unexpected character
            }
        }
 
        this.siteId = this._charArrayToInteger(siteChars);
        this.libraryBookId = this._charArrayToInteger(bookChars);
        this.libraryTopicId = this._charArrayToInteger(topicChars);
        this.nodeId = this._charArrayToInteger(nodeChars);
    },
    
    _charArrayToInteger: function(chars) {
        if (chars.length <= 0) return 0;
        var val = parseInt(chars.join(''));
        if (isNaN(val)) return 0;
        return val;
    }
};
Ext.namespace("Ait.Aspect");

//
// 
//
Ait.Aspect.SearchQuery = function(config){

    this.text = "";
    this.start = Ait.Aspect.SearchQuery.DEFAULT_START;
    this.limit = Ait.Aspect.SearchQuery.DEFAULT_LIMIT;
    this.bookIds = Ait.Aspect.SearchQuery.DEFAULT_BOOKS;
    
    Ext.apply(this, config);
};

Ait.Aspect.SearchQuery.DEFAULT_START = 0;
Ait.Aspect.SearchQuery.DEFAULT_LIMIT = 20;
Ait.Aspect.SearchQuery.DEFAULT_BOOKS = []; //This is set by the Viewer.init() through Ait.Aspect.SearchQuery.SetDefaultBooks();

Ait.Aspect.SearchQuery.SetDefaultBooks = function(configBooks) {
    var defaultBookIds = [];
    for(var i = 0, ci = configBooks.length; i < ci; i++)
        defaultBookIds.push(configBooks[i].BookId);
    Ait.Aspect.SearchQuery.DEFAULT_BOOKS = defaultBookIds;
}

//
// Static Constructor
//
Ait.Aspect.SearchQuery.FromHash = function(hash) {
    var sq = new Ait.Aspect.SearchQuery();
    sq._parseHash(hash);
    return sq;
};

//
// Characters that may appear at the start of a hashed search query
//
Ait.Aspect.SearchQuery.HashPrefixMatch = function(ch) {
    if(!ch || typeof ch != "string") return false;
    return (ch == "s" || ch =="q");
}

Ait.Aspect.SearchQuery.prototype = {

    getHash: function() {

        if (!this.text) return "";

        var encodedSearch = encodeURIComponent(this.text);

        //if all properties are set to default, then use basic search encoding
        if (this.start == Ait.Aspect.SearchQuery.DEFAULT_START &&
            this.limit == Ait.Aspect.SearchQuery.DEFAULT_LIMIT &&
            this._bookIdsEqual(Ait.Aspect.SearchQuery.DEFAULT_BOOKS))
            return "s" + encodedSearch;

        //advanced search encoding
        //q<pipe-seperated-book-ids>st<start>li<limit>s<search text>
        var hash = [];
        hash.push("q", this.bookIds.join("|"));
        hash.push("st", this.start);
        hash.push("li", this.limit);
        hash.push("s", encodedSearch);

        return hash.join("");
    },

    _parseHash: function(hash) {

        if (!hash || hash.length < 2) return null;

        //basic search encoding - query text only
        if (hash.charAt(0).toLowerCase() == "s") {
            this.text = decodeURIComponent(hash.substr(1));
            return;
        }

        //advanced search encoding
        //q<pipe-seperated-book-ids>st<start>li<limit>s<search text>
        var inxQ = hash.indexOf("q"),
            inxSt = hash.indexOf("st", inxQ),
            inxLi = hash.indexOf("li", inxSt),
            inxS = hash.indexOf("s", inxLi);

        if (inxQ != 0 || inxSt < 0 || inxLi < 0 || inxS < 0 || inxS >= hash.length)
            return; //unable to parse.
            
        var inxStFirstDigit = inxSt + "st".length;
        var inxLiFirstDigit = inxLi + "li".length;

        this.text = hash.substr(inxS + 1);
        this.start = this._intOrDefault(hash.substr(inxStFirstDigit, inxLi - inxStFirstDigit), Ait.Aspect.SearchQuery.DEFAULT_START);
        this.limit = this._intOrDefault(hash.substr(inxLiFirstDigit, inxS - inxLiFirstDigit), Ait.Aspect.SearchQuery.DEFAULT_LIMIT);

        this.bookIds = [];
        var pipedBookIds = hash.substr(inxQ + 1, inxSt - (inxQ + 1));
        if (!pipedBookIds) return;

        var aryBookIds = pipedBookIds.split("|");
        for (var i = 0, ci = aryBookIds.length; i < ci; i++) {
            var value = this._intOrDefault(aryBookIds[i], 0);
            if(value)
                this.bookIds.push(value);
        }
    },

    equals: function(otherQuery) {

        if (!otherQuery) return false;

        return this.text == otherQuery.text &&
                this.start == otherQuery.start &&
                this.limit == otherQuery.limit &&
                this._bookIdsEqual(otherQuery.bookIds);
    },

    _bookIdsEqual: function(otherBookIds) {

        if (this.bookIds.length != otherBookIds.length)
            return false;

        for (var i = 0, ci = this.bookIds.length; i < ci; i++)
            if (this.bookIds[i] != otherBookIds[i])
            return false;

        return true;
    },

    _intOrDefault: function(strInt, defaultValue) {
        var value = parseInt(strInt);
        return (isNaN(value)) ? defaultValue : value;
    }
}
