/**
* Copyright (c) 2014, Oracle and/or its affiliates.
* All rights reserved.
*/
/**
* An array based implementation of the DataGridDataSource.
* @param {Array|Object} data the data in the form of array or observable array.
* @param {Object=} options the options specific to this DataGridDataSource.
* @param {Array=} options.columns an array of columns to return as column headers.
* @param {string=} options.rowHeader the key to the header designated as the row header.
* @export
* @constructor
* @extends oj.DataGridDataSource
*/
oj.ArrayDataGridDataSource = function(data, options)
{
var errSummary, errDetail;
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.
errSummary = '_ERR_DATA_INVALID_TYPE_SUMMARY';
errDetail = '_ERR_DATA_INVALID_TYPE_DETAIL';
throw new Error(errSummary + '\n' + errDetail);
}
if (options != null)
{
this.rowHeaderKey = options['rowHeader'];
this.columns = options['columns'];
}
oj.ArrayDataGridDataSource.superclass.constructor.call(this, data);
};
// Subclass from oj.DataGridDataSource
oj.Object.createSubclass(oj.ArrayDataGridDataSource, oj.DataGridDataSource, "oj.ArrayDataGridDataSource");
/**
* Initial the array based data source.
* @export
*/
oj.ArrayDataGridDataSource.prototype.Init = function()
{
// suck out the column definition from data
if (this.columns == null)
{
this.columns = this._getColumnsForScaffolding(this.getDataArray());
}
this._initializeRowKeys();
//if the data is an observable array subscribe to array change notifications
if (typeof (this.data) == 'function')
{
this.data['subscribe'](this._subscribe.bind(this), null, 'arrayChange');
}
// call super
oj.ArrayDataGridDataSource.superclass.Init.call(this);
};
/**
* Initialize the generated row keys.
* @private
*/
oj.ArrayDataGridDataSource.prototype._initializeRowKeys = function()
{
var data;
data = this.getDataArray();
for (this.lastKey = 0; this.lastKey < data.length; this.lastKey += 1)
{
//inject the row key into the object
data[this.lastKey]['ojKey'] = this.lastKey.toString();
}
};
/**
* Get the column headers from the data, if it is an array of arrays with no row header key set,
* gets the column number as the column header.
* @param {Object} data the data to extract the column information.
* @return {Array} the columns extracted from the data.
* @private
*/
oj.ArrayDataGridDataSource.prototype._getColumnsForScaffolding = function(data)
{
var propertyName, columns;
if ((typeof data.length !== 'number') || data.length === 0)
{
return [];
}
columns = [];
for (propertyName in data[0])
{
if (data[0].hasOwnProperty(propertyName))
{
if (!(this.rowHeaderKey != undefined && propertyName == this.rowHeaderKey))
{
columns.push(propertyName);
}
}
}
return columns;
};
/**
* Returns the total number of rows or columns. If the value return is not >= 0 then it is automatically assumed
* that the total count is unknown.
* @param {string} axis the axis in which we inquire for the total count. Valid values are "row" and "column".
* @return {number} the total number of rows/columns.
* @export
*/
oj.ArrayDataGridDataSource.prototype.getCount = function(axis)
{
if (axis === "row")
{
return this._size();
}
if (axis === "column")
{
return this.columns.length;
}
return 0;
};
/**
* Retrieve the data for the header of a specified index.
* @param {string} axis the axis of the header. Valid values are "row" and "column".
* @param {number} index the index in which to get the data.
* @private
*/
oj.ArrayDataGridDataSource.prototype._getHeaderData = function(axis, index)
{
var data;
if (axis === 'row')
{
data = this.getDataArray();
// if row header is specified
if (this.rowHeaderKey != undefined)
{
return data[index][this.rowHeaderKey];
}
else if (data.length > 0 && data[0] instanceof Array)
{
// generate default row header for two dimensional array
if (this._getRowKeyByIndex(index) === undefined)
{
return index.toString();
}
else
{
return this._getRowKeyByIndex(index);
}
}
else
{
return null;
}
}
else if (axis === 'column')
{
return this.columns[index];
}
};
/**
* Retrieve the metadata for the header of a specified index.
* @param {string} axis the axis of the header. Valid values are "row" and "column".
* @param {number} index the index in which to get the metadata.
* @private
*/
oj.ArrayDataGridDataSource.prototype._getHeaderMetadata = function(axis, index)
{
if (axis === 'row')
{
if (this.rowHeaderKey != undefined)
{
return {'key': this._getRowKeyByIndex(index)};
}
}
return {'key': this._getHeaderData(axis, index)};
};
/**
* Fetch a range of headers from the data source.
* @param {Object} headerRange information about the header range, it must contain the following properties:
* axis, start, count.
* @param {string} headerRange.axis the axis of the header that are fetched. Valid values are "row" and "column".
* @param {number} headerRange.start the start index of the range in which the header data are fetched.
* @param {number} headerRange.count the size of the range in which the header data are fetched.
* @param {Object} callbacks the callbacks to be invoke when fetch headers operation is completed. The valid callback
* types are "success" and "error".
* @param {function(HeaderSet)} callbacks.success the callback to invoke when fetch headers completed successfully.
* @param {function({status: Object})} callbacks.error the callback to invoke when fetch cells failed.
* @param {Object=} callbackObjects the object in which the callback function is invoked on. This is optional.
* You can specify the callback object for each callbacks using the "success" and "error" keys.
* @export
*/
oj.ArrayDataGridDataSource.prototype.fetchHeaders = function(headerRange, callbacks, callbackObjects)
{
var axis, start, count, end, headerSet, data;
axis = headerRange.axis;
start = headerRange.start;
count = headerRange.count;
oj.Assert.assert(axis === 'row' || axis === 'column');
oj.Assert.assert(start < this.getCount(axis));
oj.Assert.assert(count > 0);
start = Math.max(0, start);
if (axis === "column")
{
end = Math.min(this.columns.length, start + count);
}
else
{
data = this.getDataArray();
// check if no row header is available
if (this.rowHeaderKey === undefined && !(data.length > 0 && data[0] instanceof Array))
{
// header count = 0
end = start;
}
else
{
end = Math.min(data.length, start + count);
}
}
headerSet = new oj.ArrayHeaderSet(start, end, axis, this);
if (callbacks != null && callbacks['success'] != null)
{
// make sure callbackObjects is not null
if (callbackObjects == null)
{
callbackObjects = {};
}
callbacks['success'].call(callbackObjects['success'], headerSet, headerRange);
}
};
/**
* Retrieve the data for the cell of a specified indexes.
* @param {number} row the row index in which to get the data.
* @param {number} column the column index in which to get the data.
* @private
*/
oj.ArrayDataGridDataSource.prototype._getCellData = function(row, column)
{
var col = this.columns[column];
return this.getDataArray()[row][col];
};
/**
* Retrieve the metadata for the cell of a specified indexes.
* @param {number} row the row index in which to get the data.
* @param {number} column the column index in which to get the data.
* @private
*/
oj.ArrayDataGridDataSource.prototype._getCellMetadata = function(row, column)
{
var keys = {"row": this._getRowKeyByIndex(row), "column": this.columns[column]};
return {"keys": keys};
};
/**
* Fetch a range of cells from the data source.
* @param {Array.<Object>} cellRanges Information about the cell range. A cell range is defined by an array
* of range info for each axis, where each range contains three properties: axis, start, count.
* @param {string} cellRanges.axis the axis associated with this range where cells are fetched. Valid
* values are "row" and "column".
* @param {number} cellRanges.start the start index of the range for this axis in which the cells are fetched.
* @param {number} cellRanges.count the size of the range for this axis in which the cells are fetched.
* @param {Object} callbacks the callbacks to be invoke when fetch cells operation is completed. The valid callback
* types are "success" and "error".
* @param {function(CellSet)} callbacks.success the callback to invoke when fetch cells completed successfully.
* @param {function({status: Object})} callbacks.error the callback to invoke when fetch cells failed.
* @param {Object=} callbackObjects the object in which the callback function is invoked on. This is optional.
* You can specify the callback object for each callbacks using the "success" and "error" keys.
* @export
*/
oj.ArrayDataGridDataSource.prototype.fetchCells = function(cellRanges, callbacks, callbackObjects)
{
var i, cellRange, rowStart, rowEnd, cellSet, colStart, colEnd;
// extract the start and end row/column info from cellRanges (there should only be two, one for each axis)
for (i = 0; i < cellRanges.length; i += 1)
{
cellRange = cellRanges[i];
oj.Assert.assert(cellRange['axis'] === 'row' || cellRange['axis'] === 'column');
oj.Assert.assert(cellRange['start'] < this.getCount(cellRange['axis']));
oj.Assert.assert(cellRange['count'] > 0);
if (cellRange['axis'] === "row")
{
rowStart = cellRange['start'];
rowEnd = Math.min(this._size(), rowStart + cellRange['count']);
}
else if (cellRange['axis'] === "column")
{
colStart = cellRange['start'];
colEnd = Math.min(this.columns.length, colStart + cellRange['count']);
}
}
// check for errors
if (rowEnd === undefined || colEnd === undefined)
{
if (callbacks != null && callbacks['error'] != null)
{
// make sure callbackObjects is not null
if (callbackObjects == null)
{
callbackObjects = {};
}
callbacks['error'].call(callbackObjects['error']);
}
return;
}
cellSet = new oj.ArrayCellSet(rowStart, rowEnd, colStart, colEnd, this);
if (callbacks != null && callbacks['success'] != null)
{
// make sure callbackObjects is not null
if (callbackObjects == null)
{
callbackObjects = {};
}
callbacks['success'].call(callbackObjects['success'], cellSet, cellRanges);
}
};
/**
* Returns the keys based on the indexes.
* @param {Object} indexes the index for each axis
* @param {string|number|null} indexes.row the index for the row axis
* @param {string|number|null} indexes.column the index for the column axis
* @return {Promise} a Promise object which upon resolution will pass in an object containing the keys for each axis
* @export
*/
oj.ArrayDataGridDataSource.prototype.keys = function(indexes)
{
var rowIndex = indexes['row'], columnIndex = indexes['column'];
return new Promise(function(resolve, reject) {
resolve({"row": this._getRowKeyByIndex(rowIndex), "column": this.columns[columnIndex]});
}.bind(this));
};
/**
* Returns the row and column index based on the keys. In a paging case returns the
* index on the page, not the absolute index in the array.
* @param {Object} keys the key for each axis
* @param {string|number|null} keys.row the key for the row axis
* @param {string|number|null} keys.column the key for the column axis
* @return {Promise} a promise object containing the index for each axis
* @export
*/
oj.ArrayDataGridDataSource.prototype.indexes = function(keys)
{
var rowKey = keys['row'], columnKey = keys['column'];
return new Promise(function(resolve, reject) {
resolve({"row": this._getRowIndexByKey(rowKey), "column": this.columns.indexOf(columnKey)});
}.bind(this));
};
/**
* Performs a sort on the data source.
* @param {Object} criteria the sort criteria.
* @param {string} criteria.axis The axis in which the sort is performed, valid values are "row", "column"
* @param {Object} criteria.key The key that identifies which header to sort
* @param {string} criteria.direction the sort direction, valid values are "ascending", "descending", "none" (default)
* @param {Object} callbacks the callbacks to be invoke upon completion of the sort operation. The callback
* properties are "success" and "error".
* @param {function()} callbacks.success the callback to invoke when the sort completed successfully.
* @param {function({status: Object})} callbacks.error the callback to invoke when sort failed.
* @param {Object=} callbackObjects the object in which the callback function is invoked on. This is optional.
* You can specify the callback object for each callbacks using the "success" and "error" properties.
* @export
*/
oj.ArrayDataGridDataSource.prototype.sort = function(criteria, callbacks, callbackObjects)
{
var sortArray = [], newColumns = [], i, headerIndex, axis, headerKey, direction;
// make sure callbackObjects is non null
if (callbacks != null && callbackObjects == null)
{
callbackObjects = {};
}
// reset sort order if no criteria is specified
if (criteria == null)
{
this._resetSortOrder(callbacks, callbackObjects);
return;
}
axis = criteria['axis'];
headerKey = criteria['key'];
direction = criteria['direction'];
if (axis === 'column')
{
// keep a copy of the original unsorted array. Both array and observable array have slice method.
if (this.origData == undefined)
{
this.origData = this.data.slice();
}
this.getDataArray().sort(this._naturalSort(direction, headerKey));
if (callbacks != null && callbacks['success'] != null)
{
callbacks['success'].call(callbackObjects['success']);
}
}
else if (axis === 'row')
{
headerIndex = this._getRowIndexByKey(headerKey);
//rebuild the array to sort on
for (i = 0; i < this.columns.length; i += 1)
{
sortArray[i] = this.getDataArray()[headerIndex][this.columns[i]];
}
//sort the given array with no headerKye specified
sortArray.sort(this._naturalSort(direction));
//reorder the columns property
for (i = 0; i < this.columns.length; i += 1)
{
newColumns[i] = this.columns[sortArray.indexOf(this.getDataArray()[headerIndex][this.columns[i]])];
}
// keep a copy of the original column order.
this.origColumns = this.columns;
this.columns = newColumns;
if (callbacks != null && callbacks['success'] != null)
{
callbacks['success'].call(callbackObjects['success']);
}
}
else
{
if (callbacks !== null && callbacks['error'] != null)
{
callbacks['error'].call(callbackObjects['error'], "Invalid axis value");
}
}
};
/**
* Reset the sort order of the data.
* @param {Object} callbacks the callbacks to be invoke upon completion of the sort operation. The callback
* properties are "success" and "error".
* @param {function()} callbacks.success the callback to invoke when the sort completed successfully.
* @param {function({status: Object})} callbacks.error the callback to invoke when sort failed.
* @param {Object=} callbackObjects the object in which the callback function is invoked on. This is optional.
* You can specify the callback object for each callbacks using the "success" and "error" properties.
* @private
*/
oj.ArrayDataGridDataSource.prototype._resetSortOrder = function(callbacks, callbackObjects)
{
// reset data to the unsorted array
if (this.origData != null)
{
this.data = this.origData;
}
// reset column order if row header was sorted before
if (this.origColumns != null)
{
this.columns = this.origColumns;
}
if (callbacks != null && callbacks['success'] != null)
{
callbacks['success'].call(callbackObjects['success']);
}
};
/**
* Determines whether this DataGridDataSource supports certain feature.
* @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
*/
oj.ArrayDataGridDataSource.prototype.getCapability = function(feature)
{
if (feature === 'sort')
{
// array based data source supports column sorting only
return 'column';
}
if (feature === 'move')
{
return 'row';
}
return null;
};
/**
* Get a comparator fuicntion for natural sorting of objects
* @param {string} direction ascending, descending
* @param {string|number=} key the key or index to perform the sort on
* @returns {function(Object, Object)|undefined} a comapartor function, dependent on direction
* @private
*/
oj.ArrayDataGridDataSource.prototype._naturalSort = function(direction, key)
{
if (direction === 'ascending')
{
return function(a, b)
{
var as, bs;
//Get the values the array we're sorting
if (key != undefined)
{
//if the sorting item is an array it will be indexed with strings of ints and needs
//to be accessed using ints not strings
if (a instanceof Array)
{
a = a[parseInt(key, 10)];
b = b[parseInt(key, 10)];
}
else
{
a = a[key];
b = b[key];
}
}
//Strings of numbers return false, so we can compare strings of numebers with numbers
as = isNaN(a);
bs = isNaN(b);
//If they are strings, check to see if they are dates, if they are, turn the string to a sortable date formatted string
if (a instanceof Date) {
a = a.toISOString();
as = true;
}
if (b instanceof Date) {
b = b.toISOString();
bs = true;
}
//both are string
if (as && bs)
{
return a < b ? -1 : a === b ? 0 : 1;
}
//only a is a string
if (as)
{
return 1;
}
//only b is a string
if (bs)
{
return -1;
}
//both are numbers
return a - b;
};
}
if (direction === 'descending')
{
return function(a, b)
{
var as, bs;
if (key != undefined)
{
//if the sorting item is an array it will be indexed with strings of ints and needs
//to be accessed using ints not strings
if (a instanceof Array)
{
a = a[parseInt(key, 10)];
b = b[parseInt(key, 10)];
}
else
{
a = a[key];
b = b[key];
}
}
as = isNaN(a);
bs = isNaN(b);
if (a instanceof Date) {
a = a.toISOString();
as = true;
}
if (b instanceof Date) {
b = b.toISOString();
bs = true;
}
if (as && bs)
{
return a > b ? -1 : a === b ? 0 : 1;
}
if (as)
{
return -1;
}
if (bs)
{
return 1;
}
return b - a;
};
}
// only if direction is not recognized
return;
};
/**
* Moves a row from one location to another.
* @param {Object} moveKey the key of the row to move
* @param {Object} atKey the key of the reference row which combined with position are used to determine
* the destination of where the row should moved to.
* @param {string} position The position of the moved row relative to the reference row.
* Valid values are: "before", "after"
* @param {function()} callbacks.success the callback to invoke when the move completed successfully.
* @param {function({status: Object})} callbacks.error the callback to invoke when move failed.
* @param {Object=} callbackObjects the object in which the callback function is invoked on. This is optional.
* You can specify the callback object for each callbacks using the "success" and "error" properties.
* @export
*/
oj.ArrayDataGridDataSource.prototype.move = function(moveKey, atKey, position, callbacks, callbackObjects)
{
var moveKeyIndex, moveData, atKeyIndex, event, data;
//remove the data from the array, but hold on to it
moveKeyIndex = this._getRowIndexByKey(moveKey);
moveData = this.data.splice(moveKeyIndex, 1)[0];
//fire the delete event to the datagrid
if (this.data instanceof Array)
{
event = this._getModelEvent('delete', moveKey, null, moveKeyIndex, -1, true);
this.handleEvent("change", event);
}
//add the stored data back into the array
if (atKey === null)
{
this.data.push(moveData);
}
else
{
atKeyIndex = this._getRowIndexByKey(atKey);
this.data.splice(atKeyIndex, 0, moveData);
}
//fire the insert event to the datagrid
if (this.data instanceof Array)
{
event = this._getModelEvent('insert', moveKey, null, atKeyIndex, -1);
this.handleEvent("change", event);
}
// if we keep track of original data, we'll need to update it
if (this.origData != null)
{
// note that once a row is moved then the current sort order is the new unsorted order
this.origData = this.data.slice();
}
};
/**
* Gets the data array, if the data property is a function call it, else return data
* @return {Object|Array} the array of the data
* @private
*/
oj.ArrayDataGridDataSource.prototype.getDataArray = function()
{
if (typeof (this.data) === 'function')
{
return this.data();
}
return this.data;
};
/**
* Gets the row index of a given row key
* @param {string|number|Object|null} key the key to get row index of
* @return {number} the index with a certain key, -1 if the key doesn't exist
* @private
*/
oj.ArrayDataGridDataSource.prototype._getRowIndexByKey = function(key)
{
var i, data = this.getDataArray();
for (i = 0; i < data.length; i++)
{
if (data[i]['ojKey'] === key)
{
return i;
}
}
return -1;
};
/**
* Gets the row key stored at a given index
* @param {number} index the index to get row key of
* @return {string|number|null} the key at index, null if the index doesn't exist
* @private
*/
oj.ArrayDataGridDataSource.prototype._getRowKeyByIndex = function(index)
{
var data = this.getDataArray();
if (data[index])
{
return data[index]['ojKey'];
}
return null;
};
/**
* Returns an Object for an event
* @param {string} operation the operation done on the model
* @param {Object|string|number|null} rowKey the key for the row axis
* @param {Object|string|number|null} columnKey the key for the column axis
* @param {number=} rowIndex the index for the row axis
* @param {number=} columnIndex the index for the column axis
* @param {boolean=} silent should the event be silent
* @return {Object} an object containing the the source, operation, and keys of the event
* @private
*/
oj.ArrayDataGridDataSource.prototype._getModelEvent = function(operation, rowKey, columnKey, rowIndex, columnIndex, silent)
{
var event = {};
event['source'] = this;
event['operation'] = operation;
event['keys'] = {'row': rowKey, 'column': columnKey};
event['indexes'] = {'row': rowIndex, 'column': columnIndex};
event['silent'] = silent;
return event;
};
/**
* Subscribe to knockout events
* @param {Array} changes an array of change objects fired by an observable array
* @private
*/
oj.ArrayDataGridDataSource.prototype._subscribe = function(changes)
{
var i, rowData, rowKey, rowIndex, added = false, move = false, keys = [], indexes = [], event, beforeDelCount = 0, change;
// first loop though the changes,
for (i = 0; i < changes.length; i++)
{
change = changes[i];
// if a model was moved using a reverseAll or a sort, just refresh the grid
if (change['moved'] !== undefined)
{
move = true;
event = this._getModelEvent('refresh', null, null);
this.handleEvent("change", event);
break;
}
// check if there were any adds, this way the delete will know to be fired silently
if (change['status'] === 'added')
{
added = true;
}
}
//if we moved a model we just refreshed
if (!move)
{
//loop through changes looking for deletes
for (i = 0; i < changes.length; i++)
{
change = changes[i];
if (change['status'] === 'deleted')
{
rowData = change['value'];
rowIndex = change['index'];
rowKey = rowData['ojKey'];
//collect the deletes to do in one batch delete
keys.push({'row': rowKey, 'column': -1});
indexes.push({'row': rowIndex, 'column': -1});
}
}
// batch delete all deletes
if (keys.length > 0)
{
event = {'source': this, 'operation': 'delete', 'keys': keys, 'indexes': indexes, 'silent': added};
this.handleEvent("change", event);
}
//loop through changes looking for adds
for (i = 0; i < changes.length; i++)
{
change = changes[i];
if (change['status'] === 'added')
{
rowData = change['value'];
rowIndex = change['index'];
//if no key add inject one into the add object based on the last assigned key
if (rowData['ojKey'] == null)
{
rowData['ojKey'] = this.lastKey.toString();
this.lastKey++;
}
//add at the given index and remove from the end of the page silently
rowKey = rowData['ojKey'];
event = this._getModelEvent('insert', rowKey, null, rowIndex, -1);
this.handleEvent("change", event);
}
}
}
// if we keep track of original data, we'll need to update it
if (this.origData != null)
{
// note that once the observable array is updated then the current sort order is the new unsorted order
this.origData = this.data.slice();
}
};
/**
* Get the length of the collection. -1 if an initial fetch has not been
* done yet. Default to the size of the collection. If pageSize is set then
* limit it.
* @returns {number} length of the collection
* @private
*/
oj.ArrayDataGridDataSource.prototype._size = function()
{
return this.getDataArray()['length'];
};
//////// testing methods to get properties /////////
/**
* Gets the rowHeaderKey property. This is an internal method for testing and should not be used by application.
* @return {string|null} the row header key
* @export
* @ignore
*/
oj.ArrayDataGridDataSource.prototype.getRowHeaderKey = function()
{
return this.rowHeaderKey;
};
/**
* Gets the columns property. This is an internal method for testing and should not be used by application.
* @return {Array|null} the keys of the column headers
* @export
* @ignore
*/
oj.ArrayDataGridDataSource.prototype.getColumns = function()
{
return this.columns;
};
/**
* Gets the data property. This is an internal method for testing and should not be used by application.
* @return {Array|Object|null} the underlying array data.
* @export
* @ignore
*/
oj.ArrayDataGridDataSource.prototype.getData = function()
{
return this.data;
};
Source: src/main/javascript/oracle/oj/ojdatagrid/ArrayDataGridDataSource.js
Oracle® JavaScript Extension Toolkit (JET)
1.1.2
E65298-01