/**
* Copyright (c) 2014, Oracle and/or its affiliates.
* All rights reserved.
*/
/**
* @preserve Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
/*jslint browser: true,devel:true*/
/**
* @export
* @class oj.ArrayTableDataSource
* @extends oj.TableDataSource
* @classdesc Object representing data used by table component
* @param {Array|Object|function():Array} data data supported by the components
* @param {Object|null} options Array of options for the TableDataSource
* @constructor
*/
oj.ArrayTableDataSource = function(data, options)
{
// Initialize
this.data = {}; // This was put in to keep closure happy...
if (!(data instanceof Array) &&
(typeof (data) != 'function' &&
typeof (data.subscribe) != 'function'))
{
// we only support Array or ko.observableArray. To
// check for observableArray, we can't do instanceof check because it's
// a function. So we just check if it contains a subscribe function.
var errSummary = oj.TableDataSource._LOGGER_MSG['_ERR_DATA_INVALID_TYPE_SUMMARY'];
var errDetail = oj.TableDataSource._LOGGER_MSG['_ERR_DATA_INVALID_TYPE_DETAIL'];
throw new Error(errSummary + '\n' + errDetail);
}
if (options == null || options['idAttribute'] == null)
{
oj.Logger.info(oj.ArrayTableDataSource._LOGGER_MSG['_INFO_ARRAY_TABLE_DATASOURCE_IDATTR']);
}
oj.ArrayTableDataSource.superclass.constructor.call(this, data, options);
this._eventHandlers = [];
this._rows = [];
if (data != null && data !== undefined)
{
this._idAttribute = null;
if (options != null && options['idAttribute'] != null)
{
this._idAttribute = options['idAttribute'];
}
this._data = (data instanceof Array) ? data : (/** @type {Function} */(data))();
this._rows = this._getRowArray(this._data);
this._totalSize = this._data.length;
if (!(data instanceof Array))
{
var self = this;
// subscribe to observableArray arrayChange event to get individual updates
(/** @type {{subscribe: Function}} */(data))['subscribe']
(
function(changes)
{
var i, j;
// do two passes, first for deletes and the second for adds
var rowArray = [];
var indexArray = [];
for (i = 0; i < changes.length; i++)
{
if (changes[i]['status'] === 'deleted')
{
rowArray.push(changes[i].value);
}
}
self._removeInternal(rowArray, null);
rowArray = [];
indexArray = [];
for (i = 0; i < changes.length; i++)
{
if (changes[i]['status'] === 'added')
{
rowArray.push(changes[i].value);
indexArray.push(changes[i].index);
}
}
self.add(rowArray, {'at': indexArray});
}, null, 'arrayChange');
}
}
if ((options != null && (options['startFetch'] == 'enabled' || options['startFetch'] == null))
|| options == null)
{
this._startFetchEnabled = true;
}
};
// Subclass from oj.DataSource
oj.Object.createSubclass(oj.ArrayTableDataSource, oj.TableDataSource, "oj.ArrayTableDataSource");
/**
* Initializes the instance.
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.Init = function()
{
oj.ArrayTableDataSource.superclass.Init.call(this);
};
/**
* Add a row (or array of rows) to the end
*
* @param {Object} m Row object data (or array of rows) to add. These should be sets of attribute/values.
* @param {Object=} options silent: if set, do not fire an add event<p>
* at: splice the new row at the value given (at:index). If an array of rows then this should be an array of indexes <p>
* @return {Promise} Promise object resolves to a compound object which contains an array of row data objects, an array of keys, and an array of indexes which were added triggering done when complete.<p>
* The structure of the resolved compound object is:<p>
* <table>
* <tbody>
* <tr><td><b>data</b></td><td>An array of raw row data</td></tr>
* <tr><td><b>keys</b></td><td>An array of key values for the rows</td></tr>
* <tr><td><b>indexes</b></td><td>An array of index values for the rows</td></tr>
* </tbody>
* </table>
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.add = function(m, options)
{
options = options || {};
var index = options['at'];
return this._addToRowSet(m, index, options);
};
/**
* Return the row data found at the given index.
*
* @param {number} index Index for which to return the row data.
* @param {Object=} options Options to control the at.
* @param {number} options.fetchSize fetch size to use if the call needs to fetch more records from the server, if virtualized. Overrides the overall fetchSize setting <p>
* @return {Promise} Promise resolves to a compound object which has the structure below. If the index is out of range, Promise resolves to null.<p>
* <table>
* <tbody>
* <tr><td><b>data</b></td><td>The raw row data</td></tr>
* <tr><td><b>index</b></td><td>The index for the row</td></tr>
* <tr><td><b>key</b></td><td>The key value for the row</td></tr>
* </tbody>
* </table>
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.at = function(index, options)
{
var row;
if (index < 0 || index >= this._rows.length)
{
row = null;
}
else
{
row = {'data': this._rows['data'][index], 'index': index, 'key': this._rows['keys'][index]};
}
return new Promise(function(resolve, reject) {
resolve(row);
});
};
/**
* Change a row (or array of rows), if found.
* @param {Object} m Row object data (or array of rows) to change. These should be sets of attribute/values.
* @param {Object=} options silent: if set, do not fire a change event<p>
* @return {Promise} Promise object resolves to a compound object which contains an array of row data objects, an array of keys, and an array of indexes which were changed triggering done when complete.<p>
* The structure of the resolved compound object is:<p>
* <table>
* <tbody>
* <tr><td><b>data</b></td><td>An array of raw row data</td></tr>
* <tr><td><b>keys</b></td><td>An array of key values for the rows</td></tr>
* <tr><td><b>indexes</b></td><td>An array of index values for the rows</td></tr>
* </tbody>
* </table>
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.change = function(m, options)
{
options = options || {};
var silent = options['silent'];
var i, row, key, changedRow, rowArray = {};
rowArray['data'] = [];
rowArray['keys'] = [];
rowArray['indexes'] = [];
if (!(m instanceof Array))
{
m = [m];
}
for (i = 0; i < m.length; i++)
{
row = m[i];
if (row != null)
{
key = this._getId(row);
changedRow = this._getInternal(key, null);
rowArray['data'].push(row);
rowArray['keys'].push(changedRow['key']);
rowArray['indexes'].push(changedRow['index']);
this._rows['data'][changedRow['index']] = row;
}
}
if (!silent && rowArray['data'].length > 0)
{
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['CHANGE'], rowArray);
}
return Promise.resolve(rowArray);
};
/**
* Fetch the row data.
* @param {Object=} options Options to control fetch
* @param {number} options.startIndex The index at which to start fetching records.
* @param {boolean} options.silent If set, do not fire a sync event.
* @return {Promise} Promise object resolves to a compound object which contains an array of row data objects, an array of ids, and the startIndex triggering done when complete.<p>
* The structure of the resolved compound object is:<p>
* <table>
* <tbody>
* <tr><td><b>data</b></td><td>An array of raw row data</td></tr>
* <tr><td><b>keys</b></td><td>An array of key values for the rows</td></tr>
* <tr><td><b>startIndex</b></td><td>The startIndex for the returned set of rows</td></tr>
* </tbody>
* </table>
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.fetch = function(options)
{
options = options || {};
var fetchType = options['fetchType'];
if (fetchType == 'init' && !this._startFetchEnabled)
{
return Promise.resolve();
}
return this._fetchInternal(options);
};
/**
* Return the first row data whose id value is the given id
* @param {string} id ID for which to return the row data, if found.
* @param {Object=} options Options to control the get.
* @return {Promise} Promise which resolves to a compound object which has the structure below where the id matches the given id. If none are found, resolves to null.<p>
* <table>
* <tbody>
* <tr><td><b>data</b></td><td>The raw row data</td></tr>
* <tr><td><b>index</b></td><td>The index for the row</td></tr>
* <tr><td><b>key</b></td><td>The key value for the row</td></tr>
* </tbody>
* </table>
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.get = function(id, options)
{
return Promise.resolve(this._getInternal(id, options));
};
/**
* Determines whether this TableDataSource supports certain feature.
* @override
* @param {string} feature the feature in which its capabilities is inquired. Currently the only valid feature is "sort".
* @return {string|null} the name of the feature. For "sort", the valid return values are: "full", "none".
* Returns null if the feature is not recognized.
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.getCapability = function(feature)
{
return 'full';
};
/**
* Remove a row (or array of rows), if found.
* @param {Object} m Row object data (or array of rows) to remove. These should be sets of attribute/values.
* @param {Object=} options silent: if set, do not fire a remove event
* @return {Promise} Promise object resolves to a compound object which contains an array of row data objects, an array of keys, and an array of indexes which were removed triggering done when complete.<p>
* The structure of the resolved compound object is:<p>
* <table>
* <tbody>
* <tr><td><b>data</b></td><td>An array of raw row data</td></tr>
* <tr><td><b>keys</b></td><td>An array of key values for the rows</td></tr>
* <tr><td><b>indexes</b></td><td>An array of index values for the rows</td></tr>
* </tbody>
* </table>
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.remove = function(m, options)
{
options = options || {};
return this._removeInternal(m, options);
};
/**
* Remove and replace the entire list of rows with a new set of rows, if provided. Otherwise, empty the datasource.
* @param {Object=} data Array of row objects with which to replace the data.
* @param {Object=} options user options, passed to event
* @return {Promise} promise object triggering done when complete.
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.reset = function(data, options)
{
var i;
options = options || {};
options['previousRows'] = this._rows;
var silent = options['silent'];
this._rows['data'] = [];
this._rows['keys'] = [];
this._rows['indexes'] = [];
this._totalSize = 0;
if (data != null)
{
this.add(data, {'silent' : true});
}
if (!silent)
{
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['RESET'], null);
}
return Promise.resolve();
};
/**
* Performs a sort on the data source.
* @override
* @param {Object} criteria the sort criteria.
* @param {Object} criteria.key The key that identifies which field to sort
* @param {string} criteria.direction the sort direction, valid values are "ascending", "descending", "none" (default)
* @return {Promise} promise object triggering done when complete.
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.sort = function(criteria)
{
if (criteria == null)
{
this['comparator'] = null;
return Promise.resolve();
}
var key = criteria['key'];
var direction = criteria['direction'];
var comparator = null;
if (direction == 'ascending')
{
comparator = function(row)
{
if ($.isFunction(row[key]))
{
return row[key]();
}
return row[key];
};
}
else if (direction == 'descending')
{
comparator = function(rowA, rowB) {
var a, b;
if ($.isFunction(rowA[key]))
{
a = rowA[key]();
b = rowB[key]();
}
else
{
a = rowA[key];
b = rowB[key];
}
if (a === b)
{
return 0;
}
return a > b ? -1 : 1;
};
}
this['comparator'] = comparator;
this['direction'] = direction;
var self = this;
return new Promise(function(resolve, reject) {
criteria = criteria || {};
// Check for comparator
if (!self._hasComparator())
{
return;
}
var comparator = self['comparator'];
self._rows['data'].sort(function(a, b)
{
return oj.ArrayTableDataSource._sortFunc(a, b, comparator, self);
});
self._recreateKeyArray(self._rows);
self._sorted = true;
var result = {'header': criteria['key'], 'direction': criteria['direction']};
oj.TableDataSource.superclass.handleEvent.call(self, oj.TableDataSource.EventType['SORT'], result);
resolve(result);
});
};
/**
* Return the total size of data available, including server side if not local.
* @override
* @returns {number} total size of data
* @export
* @expose
* @memberof! oj.ArrayTableDataSource
* @instance
*/
oj.ArrayTableDataSource.prototype.totalSize = function()
{
return this._totalSize;
};
oj.ArrayTableDataSource.prototype._addToRowSet = function(m, index, options)
{
var i, j, row, key;
options = options || {};
var silent = options['silent'];
var rowArray = {};
rowArray['data'] = [];
rowArray['keys'] = [];
rowArray['indexes'] = [];
if (!(m instanceof Array))
{
m = [m];
}
if (index != null && !(index instanceof Array))
{
index = [index];
}
for (i = 0; i < m.length; i++)
{
row = m[i];
if (row != null)
{
key = this._getId(row);
rowArray['data'].push(row);
rowArray['keys'].push(key);
if (this._sorted == true && this._rows['data'].length > 0)
{
var self = this;
for (j = 0; j < this._rows['data'].length; j++)
{
if (oj.ArrayTableDataSource._sortFunc(row, this._rows['data'][j], this['comparator'], self) < 0)
{
this._rows['data'].splice(j, 0, row);
this._rows['keys'].splice(j, 0, key);
rowArray['indexes'].push(j);
break;
}
else if (j == this._rows['data'].length - 1)
{
this._rows['data'].push(row);
this._rows['keys'].push(key);
rowArray['indexes'].push(j + 1);
break;
}
}
}
else
{
if (index == null)
{
this._rows['data'].push(row);
this._rows['keys'].push(key);
rowArray['indexes'].push(this._rows['data'].length - 1);
}
else
{
this._rows['data'].splice(index[i], 0, row);
this._rows['keys'].splice(index[i], 0, key);
rowArray['indexes'].push(index[i]);
}
}
this._totalSize++;
this._realignRowIndices();
}
}
if (!silent && rowArray['data'].length > 0)
{
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['ADD'], rowArray);
}
return Promise.resolve(rowArray);
}
oj.ArrayTableDataSource.prototype._fetchInternal = function(options)
{
options = options || {};
this._startFetch(options);
var pageSize;
try
{
pageSize = options['pageSize'] > 0 ? options['pageSize'] : -1;
if (!this._startIndex)
{
this._startIndex = 0;
}
// re-load the rows if empty
if (this._rows == null || this._rows.length == 0)
{
this._rows = this._getRowArray(this._data);
this._totalSize = this._data.length;
}
this._startIndex = options['startIndex'] == null ? this._startIndex : options['startIndex'];
var endIndex = oj.ArrayTableDataSource._getEndIndex(this._rows, this._startIndex, pageSize);
var rowArray = [];
var keyArray = [];
var i;
for (i = this._startIndex; i <= endIndex; i++)
{
rowArray[i - this._startIndex] = this._rows['data'][i];
keyArray[i - this._startIndex] = this._rows['keys'][i];
}
}
catch (err)
{
this._endFetch(options, null, err);
return Promise.reject(err);
}
if (endIndex < this._startIndex)
{
// this means we have no more rows at the startIndex. So adjust our
// startIndex down to indicate the last row
this._startIndex = endIndex + 1;
}
options['pageSize'] = pageSize;
options['startIndex'] = this._startIndex;
options['refresh'] = true;
var result = {'data': rowArray, 'keys': keyArray, 'startIndex': this._startIndex};
this._endFetch(options, result, null);
return Promise.resolve(result);
}
oj.ArrayTableDataSource.prototype._getInternal = function(id, options)
{
options = options || {};
var i, j, row, key;
var result = null;
for (i = 0; i < this._rows['data'].length; i++)
{
row = this._rows['data'][i];
if (row !== undefined)
{
key = this._rows['keys'][i];
if ($.isArray(key) && $.isArray(id))
{
if (key.length == id.length)
{
var equal = true;
for (j = 0; j < id.length; j++)
{
if (key[j] != id[j])
{
equal = false;
break;
}
}
if (equal)
{
result = {'data': row, 'key': key, 'index': this._rows['indexes'][i]};
}
}
}
else if (key == id)
{
result = {'data': row, 'key': key, 'index': this._rows['indexes'][i]};
}
}
}
return result;
};
oj.ArrayTableDataSource.prototype._hasComparator = function()
{
var comparator = this['comparator'];
return comparator !== undefined && comparator !== null;
};
// Realign all the indices of the rows (after sort for example)
oj.ArrayTableDataSource.prototype._realignRowIndices = function()
{
for (var i = 0; i < this._rows['data'].length; i++)
{
this._rows['indexes'][i] = i;
}
};
oj.ArrayTableDataSource.prototype._recreateKeyArray = function(rows)
{
var i;
for (i = 0; i < rows['data'].length; i++)
{
rows['keys'][i] = this._getId(rows['data'][i]);
}
};
oj.ArrayTableDataSource.prototype._removeInternal = function(m, options)
{
var i, j, row, key, deletedRow;
var self = this;
options = options || {};
var silent = options['silent'];
var rowArray = [];
rowArray['data'] = [];
rowArray['keys'] = [];
rowArray['indexes'] = [];
if (!(m instanceof Array))
{
m = [m];
}
var sortedRowArray = [];
for (i = 0; i < m.length; i++)
{
row = m[i];
if (row != null)
{
key = this._getId(row);
deletedRow = this._getInternal(key, null);
sortedRowArray.push({'data': deletedRow['data'], 'key': deletedRow['key'], 'index': deletedRow['index']});
}
}
sortedRowArray.sort(function(a, b)
{
return a['index'] - b['index'];
});
for (i = 0; i < sortedRowArray.length; i++)
{
rowArray['data'].push(sortedRowArray[i]['data']);
rowArray['keys'].push(sortedRowArray[i]['key']);
rowArray['indexes'].push(sortedRowArray[i]['index']);
}
for (i = rowArray['indexes'].length - 1; i >=0; i--)
{
this._rows['data'].splice(rowArray['indexes'][i], 1);
this._rows['keys'].splice(rowArray['indexes'][i], 1);
this._rows['indexes'].splice(rowArray['indexes'][i], 1);
this._totalSize--;
this._realignRowIndices();
}
if (!silent && rowArray['data'].length > 0)
{
oj.TableDataSource.superclass.handleEvent.call(self, oj.TableDataSource.EventType['REMOVE'], rowArray);
}
return Promise.resolve(rowArray);
};
oj.ArrayTableDataSource.prototype._setRow = function(index, row)
{
this._rows[index] = row;
row['index'] = index;
};
/**
* Indicate starting fetch
* @param {Object} options
* @private
*/
oj.ArrayTableDataSource.prototype._startFetch = function(options)
{
if (!options['silent'])
{
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['REQUEST'], {'startIndex' : options['startIndex']});
}
};
/**
* Indicate ending fetch
* @param {Object} options
* @param {Object} result Result object
* @param {Object} error Error
* @private
*/
oj.ArrayTableDataSource.prototype._endFetch = function(options, result, error)
{
if (error != null)
{
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['ERROR'], error);
}
else
{
if (!options['silent'])
{
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['SYNC'], result);
}
}
};
oj.ArrayTableDataSource.prototype._handleRowChange = function(event)
{
event['startIndex'] = this._startIndex;
oj.TableDataSource.superclass.handleEvent.call(this, oj.TableDataSource.EventType['CHANGE'], event);
};
oj.ArrayTableDataSource._compareKeys = function(keyA, keyB, direction)
{
if (direction == 'descending')
{
if (keyA < keyB)
{
return 1;
}
if (keyB < keyA)
{
return -1;
}
}
else
{
if (keyA > keyB)
{
return 1;
}
if (keyB > keyA)
{
return -1;
}
}
return 0;
};
oj.ArrayTableDataSource._getEndIndex = function(rows, startIndex, pageSize)
{
var endIndex = rows['data'].length - 1;
if (pageSize > 0)
{
endIndex = startIndex + pageSize - 1;
endIndex = endIndex > rows['data'].length - 1 ? rows['data'].length - 1 : endIndex;
}
return endIndex;
};
oj.ArrayTableDataSource._getKey = function(val, attr) {
if ($.isFunction(val[attr])) {
return val[attr]();
}
return val[attr];
};
oj.ArrayTableDataSource.prototype._getRowArray = function(values)
{
var endIndex = values.length - 1;
var rowArray = {}, i, prop;
rowArray['data'] = [];
rowArray['keys'] = [];
rowArray['indexes'] = [];
this._attributes = null;
for (i = 0; i <= endIndex; i++)
{
var clonedRowValues = {};
var rowValues = values[i];
for (prop in rowValues)
{
if (rowValues.hasOwnProperty(prop))
{
clonedRowValues[prop] = rowValues[prop];
if (i == 0)
{
if (this._attributes == null)
{
this._attributes = [];
}
this._attributes.push(prop);
}
}
}
rowArray['data'][i] = clonedRowValues;
rowArray['keys'][i] = this._getId(clonedRowValues);
rowArray['indexes'][i] = i;
}
return rowArray;
};
oj.ArrayTableDataSource.prototype._getId = function(row)
{
var id;
var idAttribute = this._getIdAttr();
if ($.type(idAttribute) === 'string')
{
id = row[idAttribute];
}
else if ($.isArray(idAttribute))
{
var i;
id = [];
for (i = 0; i < idAttribute.length; i++)
{
id[i] = row[idAttribute[i]];
}
}
return id;
};
oj.ArrayTableDataSource.prototype._getIdAttr = function()
{
if (this._idAttribute != null)
{
return this._idAttribute;
}
else
{
if (this._attributes.hasOwnProperty('id'))
{
return 'id';
}
else
{
return this._attributes;
}
}
};
oj.ArrayTableDataSource._sortFunc = function(a, b, comparator, self)
{
var keyA, keyB, i, retVal;
if ($.isFunction(comparator))
{
// How many args?
if (comparator.length === 1)
{
// "sortBy" comparator option
keyA = comparator.call(self, a);
keyB = comparator.call(self, b);
var attrs1 = oj.StringUtils.isString(keyA) ? keyA.split(",") : [keyA];
var attrs2 = oj.StringUtils.isString(keyB) ? keyB.split(",") : [keyB];
for (i = 0; i < attrs1.length; i++)
{
retVal = oj.ArrayTableDataSource._compareKeys(attrs1[i], attrs2[i], self['direction']);
if (retVal !== 0)
{
return retVal;
}
}
return 0;
}
// "sort" comparator option
return comparator.call(self, a, b);
}
// String option
if (oj.StringUtils.isString(comparator))
{
var attrs = comparator.split(",");
for (i = 0; i < attrs.length; i++)
{
keyA = oj.ArrayTableDataSource._getKey(a, attrs[i]);
keyB = oj.ArrayTableDataSource._getKey(b, attrs[i]);
retVal = oj.ArrayTableDataSource._compareKeys(keyA, keyB, self['direction']);
if (retVal !== 0)
{
return retVal;
}
}
}
return 0;
};
oj.ArrayTableDataSource._LOGGER_MSG =
{
'_INFO_ARRAY_TABLE_DATASOURCE_IDATTR': "idAttribute option has not been specified. Will default to using 'id' if the field exists. If not, will use all the fields."
};
Source: src/main/javascript/oracle/oj/ojdatacollection-common/ArrayTableDataSource.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01