// SpryHTMLDataSet.js - version 0.22 - Spry Pre-Release 1.6.1

//

// Copyright (c) 2006. Adobe Systems Incorporated.

// All rights reserved.

//

// Redistribution and use in source and binary forms, with or without

// modification, are permitted provided that the following conditions are met:

//

//   * Redistributions of source code must retain the above copyright notice,

//     this list of conditions and the following disclaimer.

//   * Redistributions in binary form must reproduce the above copyright notice,

//     this list of conditions and the following disclaimer in the documentation

//     and/or other materials provided with the distribution.

//   * Neither the name of Adobe Systems Incorporated nor the names of its

//     contributors may be used to endorse or promote products derived from this

//     software without specific prior written permission.

//

// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"

// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE

// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR

// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF

// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS

// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN

// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)

// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE

// POSSIBILITY OF SUCH DAMAGE.



//////////////////////////////////////////////////////////////////////

//

// Spry.Data.HTMLDataSet

//

//////////////////////////////////////////////////////////////////////



Spry.Data.HTMLDataSet = function(dataSetURL, sourceElementID, dataSetOptions)

{

	this.sourceElementID = sourceElementID; // ID of the html element to be used as a data source

	this.sourceElement = null;  			      // The actual html element to be used as a data source



	this.sourceWasInitialized = false;

	this.usesExternalFile = (dataSetURL != null) ? true : false;

	

	this.firstRowAsHeaders = true;

	this.useColumnsAsRows = false;

	this.columnNames = null;

	this.hideDataSourceElement = true;

	

	this.rowSelector = null;

	this.dataSelector = null;

	this.removeUnbalancedRows = true;



	this.tableModeEnabled = true;



	Spry.Data.HTTPSourceDataSet.call(this, dataSetURL, dataSetOptions);

};





Spry.Data.HTMLDataSet.prototype = new Spry.Data.HTTPSourceDataSet();

Spry.Data.HTMLDataSet.prototype.constructor = Spry.Data.HTMLDataSet;





Spry.Data.HTMLDataSet.prototype.getDataRefStrings = function() 

{

	var dep = [];

	if (this.url) 

		dep.push(this.url);

	if (typeof this.sourceElementID == "string") 

		dep.push(this.sourceElementID);

	

	return dep;

};



Spry.Data.HTMLDataSet.prototype.setDisplay = function(ele, display)

{

	if( ele )

		ele.style.display = display;

};



Spry.Data.HTMLDataSet.prototype.initDataSource = function(callLoadData)

{

	if (!this.loadDependentDataSets())

		return;

	if (!this.usesExternalFile)

	{

		this.setSourceElement();

		if (this.hideDataSourceElement)

			this.setDisplay(this.sourceElement, "none");

	}

	//this.sourceWasInitialized = true;

};





Spry.Data.HTMLDataSet.prototype.setSourceElement = function (externalDataElement)

{

   // externalDataElement is the container that holds the data imported from the external file.

	this.sourceElement = null;

	if (!this.sourceElementID) 

	{

	  if (externalDataElement)

  	  this.sourceElement = externalDataElement;

  	else

  	{

  	  this.hideDataSourceElement = false;

  	  this.sourceElement = document.body;

  	}

	  return; 

	}

	

	var sourceElementID = Spry.Data.Region.processDataRefString(null, this.sourceElementID, this.dataSetsForDataRefStrings);

	if (!this.usesExternalFile)

	   this.sourceElement = Spry.$(sourceElementID);

	else

    if (externalDataElement) 

    {

      var foundElement = false;

      // looking for the specified ID in the current element node

      var sources = Spry.Utils.getNodesByFunc(externalDataElement, function(node)

    	{

    	    if (foundElement) 

    	      return false;

    			if (node.nodeType != 1)

    				return false;

    			if (node.id && node.id.toLowerCase() == sourceElementID.toLowerCase())

    			{

    			  foundElement = true;

    			  return true;

    			}

      });

      this.sourceElement = sources[0];

    }

    

	if (!this.sourceElement) 

		Spry.Debug.reportError("Spry.Data.HTMLDataSet: '" + sourceElementID + "' is not a valid element ID");

};





Spry.Data.HTMLDataSet.prototype.getSourceElement = function() { return this.sourceElement; };

Spry.Data.HTMLDataSet.prototype.getSourceElementID = function() { return this.sourceElementID; };

Spry.Data.HTMLDataSet.prototype.setSourceElementID = function(sourceElementID)

{

	if (this.sourceElementID != sourceElementID)

	{

		this.sourceElementID = sourceElementID;

		this.recalculateDataSetDependencies();

		this.dataWasLoaded = false;

	}

};



Spry.Data.HTMLDataSet.prototype.getDataSelector = function() { return this.dataSelector; };

Spry.Data.HTMLDataSet.prototype.setDataSelector = function(dataSelector)

{ 

  if (this.dataSelector != dataSelector)

  {

     this.dataSelector = dataSelector;

  	 this.dataWasLoaded = false;

  }

};



Spry.Data.HTMLDataSet.prototype.getRowSelector = function() { return this.rowSelector; };

Spry.Data.HTMLDataSet.prototype.setRowSelector = function(rowSelector)

{ 

  if (this.rowSelector != rowSelector)

  {

     this.rowSelector = rowSelector;

  	 this.dataWasLoaded = false;

  }

};





Spry.Data.HTMLDataSet.prototype.loadDataIntoDataSet = function(rawDataDoc)

{

	var responseText = rawDataDoc;

	responseText = Spry.Data.HTMLDataSet.cleanupSource(responseText);



	var div = document.createElement("div");

	div.id = "htmlsource" + this.internalID;

	div.innerHTML = responseText;



	this.setSourceElement(div);

	if (this.sourceElement)

	{

		var parsedStructure = this.getDataFromSourceElement();

		if (parsedStructure) 

		{

			this.dataHash = parsedStructure.dataHash;

			this.data = parsedStructure.data;

		}		

	}

	this.dataWasLoaded = true;

	div = null;

};





Spry.Data.HTMLDataSet.prototype.loadDependentDataSets = function() 

{

	if (this.hasDataRefStrings)

	{

		var allDataSetsReady = true;



		for (var i = 0; i < this.dataSetsForDataRefStrings.length; i++)

		{

			var ds = this.dataSetsForDataRefStrings[i];

			if (ds.getLoadDataRequestIsPending())

				allDataSetsReady = false;

			else if (!ds.getDataWasLoaded())

			{

				// Kick off the load of this data set!

				ds.loadData();

				allDataSetsReady = false;

			}

		}



		// If our data sets aren't ready, just return. We'll

		// get called back to load our data when they are all

		// done.



		if (!allDataSetsReady)

			return false;

	}

	return true;

};





Spry.Data.HTMLDataSet.prototype.loadData = function()

{

	this.cancelLoadData();

	this.initDataSource();

	

	var self = this;

	if (!this.usesExternalFile) 

	{

		this.notifyObservers("onPreLoad");

		

		this.dataHash = new Object;

		this.data = new Array;

		this.dataWasLoaded = false;

		this.unfilteredData = null;

		this.curRowID = 0;

		

		this.pendingRequest = new Object;

		this.pendingRequest.timer = setTimeout(function()

		{

			self.pendingRequest = null;

			var parsedStructure = self.getDataFromSourceElement();

			if (parsedStructure) 

			{

				self.dataHash = parsedStructure.dataHash;

				self.data = parsedStructure.data;

			}

			self.dataWasLoaded = true;

			

			self.disableNotifications();

			self.filterAndSortData();

			self.enableNotifications();

			

			self.notifyObservers("onPostLoad");

			self.notifyObservers("onDataChanged");	

		}, 0); 

	}

	else 

	{

		var url = Spry.Data.Region.processDataRefString(null, this.url, this.dataSetsForDataRefStrings);



		var postData = this.requestInfo.postData;

		if (postData && (typeof postData) == "string") 

			postData = Spry.Data.Region.processDataRefString(null, postData, this.dataSetsForDataRefStrings);

		this.notifyObservers("onPreLoad");

		

	

		this.dataHash = new Object;

		this.data = new Array;

		this.dataWasLoaded = false;

		this.unfilteredData = null;

		this.curRowID = 0;



		var req = this.requestInfo.clone();

		req.url = url;

		req.postData = postData;

	

		this.pendingRequest = new Object;

		this.pendingRequest.data = Spry.Data.HTTPSourceDataSet.LoadManager.loadData(req, this, this.useCache);

	}

};





Spry.Data.HTMLDataSet.cleanupSource = function (source)

{

	// Cleans the content by replacing the src/href with spry_src 

	// This prevents browser to load the external resources.

  source = source.replace(/<(img|script|link|frame|iframe|input)([^>]+)>/gi, function(a,b,c) {

			//b=tag name,c=tag attributes

			return '<' + b + c.replace(/\b(src|href)\s*=/gi, function(a, b) {

				//b=attribute name

				return 'spry_'+ b + '=';

			}) + '>';

		});

	return source;

};





Spry.Data.HTMLDataSet.undoCleanupSource = function (source)

{

	// Undo cleanup. See the cleanupSource function

	source = source.replace(/<(img|script|link|frame|iframe|input)([^>]+)>/gi, function(a,b,c) {

			//b=tag name,c=tag attributes

			return '<' + b + c.replace(/\bspry_(src|href)\s*=/gi, function(a, b) {

				//b=attribute name

				return b + '=';

			}) + '>';

		});

	return source;

};





Spry.Data.HTMLDataSet.normalizeColumnName = function(colName) 

{

	if (colName)

	{

		// Removes the tags from column names values

		// Replaces spaces with underscore

		colName = colName.replace(/(?:^[\s\t]+|[\s\t]+$)/gi, "");

		colName = colName.replace(/<\/?([a-z]+)([^>]+)>/gi, "");

		colName = colName.replace(/[\s\t]+/gi, "_");

	}

	return colName;

};





Spry.Data.HTMLDataSet.prototype.getDataFromSourceElement = function() 

{

	if (!this.sourceElement) 

    return null;



	var extractedData;

	var usesTable = (this.tableModeEnabled && this.sourceElement.nodeName.toLowerCase() == "table");

	if (usesTable)

		extractedData = this.getDataFromHTMLTable();

	else

		extractedData = this.getDataFromNestedStructure();



	if (!extractedData)

     return null;



	// Flip Columns / Rows

	if (this.useColumnsAsRows) 

	{

	   var flipedData = new Array;

	   // Get columns and put them as rows 

	   for (var rowIdx = 0; rowIdx < extractedData.length; rowIdx++)

	   {

	     var row = extractedData[rowIdx];

	     for (var colIdx = 0; colIdx < row.length; colIdx++) 

	     {

	       if (!flipedData[colIdx]) flipedData[colIdx] = new Array;

	       flipedData[colIdx][rowIdx]= row[colIdx];

	     }

	   }

	   extractedData = flipedData;

	}



	// Build the data structure for the DataSet

	var parsedStructure = new Object();

	parsedStructure.dataHash = new Object;

	parsedStructure.data = new Array;

	

	if (extractedData.length == 0) 

	   return parsedStructure;



	// Find the max number of columns. We have to look at each

	// row because, rows can have varying number of columns.



	var maxColumnCount = 0;



	for (var i = 0; i < extractedData.length; i++)

	{

		var len = extractedData[i].length;

		if (maxColumnCount < len)

			maxColumnCount = len;

	}



	// Get the column names

	// this.firstRowAsHeaders is used only if the source of data is a TABLE

	var columnNames = new Array;

	var firstRowOfData = extractedData[0];



	for (var colIdx = 0; colIdx < maxColumnCount; colIdx++)

	{

		if (usesTable && this.firstRowAsHeaders)

			columnNames[colIdx] = Spry.Data.HTMLDataSet.normalizeColumnName(firstRowOfData[colIdx]);

		if (!columnNames[colIdx])

			columnNames[colIdx] = "column" + colIdx;

	}



	// Check if column names are being overwritten using the optional columnNames parameter

	if (this.columnNames && this.columnNames.length) 

	{

		var numCols = (maxColumnCount < this.columnNames.length) ? maxColumnCount : this.columnNames.length;

		for (var i = 0; i < numCols; i++) {

			if (this.columnNames[i])

				columnNames[i] = this.columnNames[i];

		}

	}

  

	// Place the extracted data into a dataset kind of structure

	var nextID = 0;

	var firstDataRowIndex = (usesTable && this.firstRowAsHeaders) ? 1: 0;

	

	for (var rowIdx = firstDataRowIndex; rowIdx < extractedData.length; rowIdx++)

	{

		var row = extractedData[rowIdx];

		if (this.removeUnbalancedRows && columnNames.length != row.length)

		{

			// Spry.Debug.reportError("Unbalanced column names for row #" + (rowIdx+1) + ". Skipping row." );

			continue;

		}

		

		var rowObj = {};

		for (var colIdx = 0; colIdx < columnNames.length; colIdx++)

		{

			var colValue = row[colIdx];

			rowObj[columnNames[colIdx]] = (typeof colValue == "undefined") ? "" : colValue;

		}



		rowObj['ds_RowID'] = nextID++;

		parsedStructure.dataHash[rowObj['ds_RowID']] = rowObj;

		parsedStructure.data.push(rowObj);

	}

	return parsedStructure;

};





Spry.Data.HTMLDataSet.getElementChildren = function(element)

{

	var children = [];

	var child = element.firstChild;

	while (child)

	{

		if (child.nodeType == 1)

			children.push(child);

		child = child.nextSibling;

	}

	return children;

};





// This method extracts data from a TABLE structure

// It knows how to handle both colspan and rowspan



Spry.Data.HTMLDataSet.prototype.getDataFromHTMLTable = function()

{

  var tHead = this.sourceElement.tHead;

  var tBody = this.sourceElement.tBodies[0];

  

  var rowsHead = [];

  var rowsBody = [];

  if (tHead) rowsHead = Spry.Data.HTMLDataSet.getElementChildren(tHead);

  if (tBody) rowsBody = Spry.Data.HTMLDataSet.getElementChildren(tBody);

  

  var extractedData = new Array;

  var rows = rowsHead.concat(rowsBody);

  if (this.rowSelector) rows = Spry.Data.HTMLDataSet.applySelector(rows, this.rowSelector);

  for (var rowIdx = 0; rowIdx < rows.length; rowIdx++)

  {

     var row = rows[rowIdx];

     

     var dataRow;

     if (extractedData[rowIdx]) dataRow = extractedData[rowIdx];

     else dataRow = new Array;

     

     var offset = 0;

     var cells = row.cells;

     if (this.dataSelector) cells = Spry.Data.HTMLDataSet.applySelector(cells, this.dataSelector);

     for (var cellIdx=0; cellIdx < cells.length; cellIdx++)

     {

       var cell = cells[cellIdx];

       var nextCellIndex = cellIdx + offset;

       

       // Find the next available position

       while (dataRow[nextCellIndex])

       {

          offset ++;

          nextCellIndex ++;

       }

       var cellValue = Spry.Data.HTMLDataSet.undoCleanupSource(cell.innerHTML);

       dataRow[nextCellIndex] = cellValue;

       

       // Handle collspan

       var colspan = cell.colSpan;

       if (colspan == 0) colspan = 1;

       var startOffset = offset;

       for (var offIdx = 1; offIdx < colspan; offIdx++)

       {

         offset ++;

         nextCellIndex = cellIdx + offset;

         dataRow[nextCellIndex] = cellValue;

       }

       

       // Handle rowspan

       var rowspan = cell.rowSpan;

       if (rowspan == 0) rowspan = 1;

       for (var rowOffIdx = 1; rowOffIdx < rowspan; rowOffIdx++)

       {

         nextRowIndex = rowIdx + rowOffIdx;

         var nextDataRow;

         if (extractedData[nextRowIndex]) nextDataRow = extractedData[nextRowIndex];

         else nextDataRow = new Array;

         

         var rowSpanCellOffset = startOffset;

         for (var offIdx = 0; offIdx < colspan; offIdx++)

         {

           nextCellIndex = cellIdx + rowSpanCellOffset;

           nextDataRow[nextCellIndex] = cellValue;

           rowSpanCellOffset ++;

         }

         extractedData[nextRowIndex] = nextDataRow;

       }

      }

     extractedData[rowIdx] = dataRow;

  }

  return extractedData;

};







// This method extracts data from any HTML structure

// It uses rowSelector and dataSelector in order to build a three level nested structure - 

// Either one: rowSelector or dataSelector can miss



Spry.Data.HTMLDataSet.prototype.getDataFromNestedStructure = function()

{

  var extractedData = new Array;

  

  if (this.sourceElementID && !this.rowSelector && !this.dataSelector) 

  {

     // The whole sourceElementID is a single row, single cell structure;

     extractedData[0] = [Spry.Data.HTMLDataSet.undoCleanupSource(this.sourceElement.innerHTML)];

     return extractedData;

  }

  

  var self = this;

  // Get the rows

  var rows = [];

  if (!this.rowSelector)

     // If no rowSelector, there will be only one row

     rows = [this.sourceElement];

  else

     rows = Spry.Utils.getNodesByFunc(this.sourceElement, function(node) { 

            return Spry.Data.HTMLDataSet.evalSelector(node, self.sourceElement, self.rowSelector); 

           }); 

           

  // Get the data columns

  for (var rowIdx = 0; rowIdx < rows.length; rowIdx++)

  {

    var row = rows[rowIdx];

    // Get the cells that actually hold the data for each row

    var cells = [];

    if (!this.dataSelector)

      // If no dataSelector, the whole row is extracted as one cell row.

      cells = [row];

    else

      cells = Spry.Utils.getNodesByFunc(row, function(node) { 

               return Spry.Data.HTMLDataSet.evalSelector(node, row, self.dataSelector); 

              });

              

    extractedData[rowIdx] = new Array;

    for (var cellIdx = 0; cellIdx < cells.length; cellIdx ++)

       extractedData[rowIdx][cellIdx] = Spry.Data.HTMLDataSet.undoCleanupSource(cells[cellIdx].innerHTML);

  }

  return extractedData;

};



// Applies a css selector on a collection and returns the resulting elements

Spry.Data.HTMLDataSet.applySelector = function(collection, selector, root)

{

   var newCollection = [];

   for (var idx = 0; idx < collection.length; idx++)

   {

     var node = collection[idx];

     if (Spry.Data.HTMLDataSet.evalSelector(node, root?root:node.parentNode, selector))

        newCollection.push(node);

   }

   return newCollection;

};



// Checks if a specified node matches the specified css selector

Spry.Data.HTMLDataSet.evalSelector = function (node, root, selector)

{

  if (node.nodeType != 1)

 		return false;

 	if (node == root)

 	  return false;

 	  

 	// Comma delimited selectors can be passed in

 	// The node is selected if it matches one of the selectors

 	// #myID1, div#myID2, #myID3

  var selectors = selector.split(",");

  for (var idx = 0; idx < selectors.length; idx ++)

  {

    var currentSelector = selectors[idx].replace(/^\s+/, "").replace(/\s+$/, "");

   	var tagName = null;

   	var className = null;

   	var id = null;

   	

   	// Accepted values for the selector:

   	// DIV.myClass | DIV | .myClass | *.myClass

   	// DIV#myID | #myID

   	// > DIV.myClass : > points to the direct descendents

   	

   	var selected = true;

   	if (currentSelector.substring(0,1) == ">") 

   	{

   	    // Looking for direct descendants only

   	    if (node.parentNode != root) 

   	      selected = false;

   	    else

   	      currentSelector = currentSelector.substring(1).replace(/^\s+/, "");

   	}

   	if (selected) 

   	{

     	tagName = currentSelector.toLowerCase();

     	if (currentSelector.indexOf(".") != -1)

     	{

     	  var parts = currentSelector.split(".");

     	  tagName = parts[0];

     	  className = parts[1];

     	}

     	else if (currentSelector.indexOf("#") != -1)

     	{

     	  var parts = currentSelector.split("#");

     	  tagName = parts[0];

     	  id = parts[1];

     	}

   	}

   	if (selected && tagName != '' && tagName != '*')

   	    if (node.nodeName.toLowerCase() != tagName) 

   	       selected = false;

   	if (selected && id && node.id != id)

   	    selected = false;

    	if (selected && className && node.className.search(new RegExp('\\b' + className + '\\b', 'i')) ==-1) 

   	    selected = false;

   	if (selected)

   	 return true;

  }

  return false;

};


