// SpryHTMLDataSet.js - version 0.18 - Spry Pre-Release 1.6
//
// 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;
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)
{
// 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 = false;
switch (this.sourceElement.nodeName.toLowerCase())
{
case "table":
usesTable = true;
extractedData = this.getDataFromHTMLTable();
break;
default:
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 cellIdx = 0; cellIdx < row.length; cellIdx++)
{
if (!flipedData[cellIdx]) flipedData[cellIdx] = new Array;
flipedData[cellIdx][rowIdx]= row[cellIdx];
}
}
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;
// 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 cellIdx=0; cellIdx < firstRowOfData.length; cellIdx++)
{
if (usesTable && this.firstRowAsHeaders) columnNames[cellIdx] = Spry.Data.HTMLDataSet.normalizeColumnName(firstRowOfData[cellIdx]);
else columnNames[cellIdx] = "column" + cellIdx;
}
// Check if column names are being overwritten using the optional columnNames parameter
if (this.columnNames && this.columnNames.length)
{
if (this.columnNames.length < columnNames.length)
Spry.Debug.reportError("Too few elements in the columnNames array. The columNames length must match the actual number of columns." );
else
for (var i=0; i < columnNames.length; 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 (columnNames.length != row.length)
{
Spry.Debug.reportError("Unbalanced column names for row #" + (rowIdx+1) + ". Skipping row." );
continue;
}
var rowObj = {};
for (var cellIdx = 0; cellIdx < row.length; cellIdx++)
rowObj[columnNames[cellIdx]] = row[cellIdx];
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;
for (var offIdx = 0; offIdx < colspan; offIdx++)
{
nextCellIndex = cellIdx + startOffset;
nextDataRow[nextCellIndex] = cellValue;
startOffset ++;
}
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;
};