﻿/// <reference name="MicrosoftAjax.js"/>


//Copyright 2008 Jordan Knight
//This code is licenced under Creative Commons Licence: http://creativecommons.org/licenses/by-sa/3.0/

//This control is built from the standard AJAX Client Control VS08 template.

Type.registerNamespace("ClientTemplate");

ClientTemplate.TemplateManager = function(element) {
    ClientTemplate.TemplateManager.initializeBase(this, [element]);
    
    //initialise some varibles that the control will use later
    this._itemTemplate = null;
    this._itemSpacer = null;
    this._targetContainer = null;
    this._itemsSource = null;  
    this._innerHTMLReplace = null; 
    this._isBinding = false;
    //this is the signature that will be added on to all "binding" functions. 
    this._funcSig = "result = function(rowContainer, dataContext, pageRowIndex)";
}

ClientTemplate.TemplateManager.prototype = {
    initialize: function() {
        ClientTemplate.TemplateManager.callBaseMethod(this, 'initialize');     
        
        
        //populate the template and spacer elements into the variables for easy access.
        this._itemTemplate = $get("itemTemplate", this.get_element());
        
        this._itemSpacer = $get("itemSpacer", this.get_element());
        
        //remove all comments as they wreck the binding stuff below for some reason :)
        this._itemTemplate.innerHTML = this._itemTemplate.innerHTML.replace(/<!--.*?-->/gi, "");        
    },
    
    dispose: function() {        
        //This control should probably do some disposal, oh well... maybe next time :)
        ClientTemplate.TemplateManager.callBaseMethod(this, 'dispose');
    },
    
    //Get and set the target container, which holds the result of a binding operation.
    get_targetContainer : function() 
    {
        return this._targetContainer;
    },
    
    set_targetContainer : function(value)
    {
        this._targetContainer = value;
    },
    
    //Get and set the items source, which holds that data that will be displayed.    
    get_itemsSource : function()
    {
        return this._itemsSource;
    },
    
    set_itemsSource : function(value)
    {
        this._itemsSource = value;
    },
    
    //Kick off a binding operation. Binding loops through all records in _itemsSource
    dataBind : function()
    {
          if(!this._isBinding)
          {
              this._isBinding = true;
              if(this._itemsSource != null)
              { 
                //Keeps track of how many records are in the page at presnt time. if there are too many template elements added, some need to be removed
                //Example: in pages of 10 and the last page only has 2 records... 8 need to be removed.
                var trimRecordThreshold = 0;
                
                var childNodeArray = this._childElements(this._targetContainer);
            
                if(childNodeArray.length > 1)
                {
                    trimRecordThreshold = this._itemsSource.length * 2; //Calculate what index to begin trimming.
                    if(childNodeArray.length > trimRecordThreshold)
                    {
                        this._trimFromIndex(trimRecordThreshold);
                    }
                }
              
                //Iterate over each item in the array 
                for(var i = 0; i < this._itemsSource.length; i++)
                {
                  this._processBindings(this._itemsSource[i], i, childNodeArray);   
                }
                
              }
              else
              {
                this._trimFromIndex(trimRecordThreshold); //Clear all as there is no data.
              }
              this._isBinding = false;
           }
    },
    
    _processBindings : function(dataContext, pageRowIndex, childNodeArray)
    {
        //Stores an offset index for the target of this particular binding iteration as this needs to take into account 
        //dividers bettween records.
        //Index is used so on a second page or data refresh, the same DOM elements can be used... so it needs to know how
        //to find them
        var myActualTargetIndex = 0;
        
        //Don't offset the actual index when this is the first record.
        if(pageRowIndex > 0)
        {
            myActualTargetIndex = pageRowIndex * 2;
        }     
       
       //Take into account that dividers have been added between records!       
       if(childNodeArray.length > myActualTargetIndex)
       {   
            myActualTarget = childNodeArray[myActualTargetIndex];
       }
       else
       {
            //If this is not the first record then add the divider template between the records.
           if(pageRowIndex != 0)
           {
                this._insertSpacer(pageRowIndex);    
           }
           //Create the copy of the template and insert into the page.
           myActualTarget = this._createTemplateCopy(pageRowIndex);
       }
       
       //Begin the recursing through the child elements of the template item.
       this._bindElement(this._itemTemplate, myActualTarget, myActualTarget, dataContext, pageRowIndex);
    },
    
    //This method walks through the template source and binding target trees simultaneously.
    //This is done so that the attibutes on the original template item can be re-read on refresh and re-applied to the target.    
    _bindElement : function(templateSource, bindingTarget, rowContainer, dataContext, pageRowIndex)
    {
        //Process the attributes on the current element.
        this._processAttributes(templateSource, bindingTarget, rowContainer, dataContext, pageRowIndex);
        
        for(var i = 0; i < templateSource.childNodes.length; i++)
        {
            //ensure the item has attributes and that it is an element nodeType
            if(templateSource.childNodes[i] && templateSource.childNodes[i].attributes && (!templateSource.childNodes[i].nodeType || templateSource.childNodes[i].nodeType == 1))
            {            
                if(!bindingTarget.childNodes[i])
                {
                    var someting = "test";
                }
                //To understand recursion, you must first understand recursion :)
                this._bindElement(templateSource.childNodes[i], bindingTarget.childNodes[i], rowContainer, dataContext, pageRowIndex);    
            }
        }
    }, 
    
    //Returns an array of child elements for the current node. This is required for Firefox as .childNodes returns all sorts of wonderful things :)
    _childElements : function(parentNode)
    {
        var result = new Array();
        
        for(var i = 0; i < parentNode.childNodes.length;i++)
        {
            var node = parentNode.childNodes[i];
            if(!node.nodeType || node.nodeType == 1)
            {
                Array.add(result, node);
            }
        }
        return result;
    },
    
    //Removes any item after the index. This is to remove extra records that where added on a previous refresh.
    //Loops through indexStart elements before it starts trimming. 
    //This is to handle browsers that use nodeType attribute (Firefox)
    _trimFromIndex : function(indexStart)
    {
        var trimPos = 0;
        var currentIndex = 0;
        
        for(var i = 0; i < this._targetContainer.childNodes.length; i++)
        {
            var node = this._targetContainer.childNodes[i];
            if(!node.nodeType)
            {
                //probably IE so no nodetypes no need to worry about all this junk :)
                trimPos = indexStart;
                break;
            }
            if(node.nodeType == 1)
            {
                currentIndex ++;             
            }
            trimPos+=1;
            
            if(currentIndex >= indexStart)
            {
                break;
            }            
        }
        while(this._targetContainer.childNodes.length >= trimPos)
        {
            this._targetContainer.removeChild(this._targetContainer.childNodes[this._targetContainer.childNodes.length - 1]);
        }
    },
    
    //Runs through attributes on the template element, checks for the presence of "binding" code.
    //Compiles into functions or loads from function cache on template element.
    //Runs function and returns result into the same attribute on the binding target.   
    _processAttributes : function (templateSource, bindingTarget, rowContainer, dataContext, pageRowIndex)
    {
        //Initialise a little cache of compiled functions on the template source so they may be re-used 
        //in subsequent items in this refresh, and in future refresh's.
        //Eval costs time, so this helps improve performance.
        if(!templateSource.compiledFunctions)
        {
            templateSource.compiledFunctions  = new Array();
        }
        
        //Visit each attribute, check and see if they have { in them, and they aren't the compiled functions cache.    
        for(var i = 0; i < templateSource.attributes.length; i ++)
        {
            var attr = templateSource.attributes[i];
            
            if(attr && attr.name != "compiledFunctions" && attr.value.indexOf("{") != -1)
            {
                
                var func = null;
                
                //If this attribute has never been processed, evaluate it and add to the cache.
                if(!templateSource.compiledFunctions[i])
                {
                    func = this._evalBindingToFunction(attr.value);                        
                    templateSource.compiledFunctions[i] = func;
                }
                else
                {
                    //If this attribute has already been processed, load it from the cache.
                    func = templateSource.compiledFunctions[i]
                }
                
                //In the {} based attributes you can use "this." etc... this call to Function.createDelegate
                //sets the scope of "this" to the element that the attribute lives on.
                //I tried storing the fuction on the templateSource and the delegate on the bindingTarget - I think
                //the way it is now is better because there are less evals being run overall (but more createDelegate).
                scopeDelegate = Function.createDelegate(bindingTarget, func);                
                
                //Run the function and set the attribute value to its result.
                //Pass in the params as defined in the function header at the top of the control... these objects can then 
                //be accessed in the code on the attributes.
                var result = scopeDelegate(rowContainer, dataContext, pageRowIndex);
                
                if(result)
                {
                    bindingTarget.attributes[i].value = result;
                }
            }
        }
    },    
   
    _createTemplateCopy : function(pageRowIndex)
    {
        //Grab the template source, wrap it in a div with a class so you can play with it in CSS, then insert it into the template container. 
        //Return reference to it.
        this._targetContainer.innerHTML += "<div class='itemContainer' id='container_" + pageRowIndex + "'>" + this._itemTemplate.innerHTML.replace(/\n/gi, "") + "</div>";
        return this._targetContainer.childNodes[this._targetContainer.childNodes.length - 1];
    },
    
    _insertSpacer : function (pageRowIndex)    
    {
        //Same as inserting template row really.
        this._targetContainer.innerHTML += "<div class='itemSpacer' id='spacer_" + pageRowIndex + "'>" + this._itemSpacer.innerHTML + "</div>";        
    },
        
    //Converts the attribute value in to a JS function, adding on the function header defined at the top of the control.
    _evalBindingToFunction : function(attrValue)
    {
        var result = null;        
        eval(this._funcSig + attrValue);
        return result;
    }   
}

ClientTemplate.TemplateManager.registerClass('ClientTemplate.TemplateManager', Sys.UI.Control);

if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
