Source: src/main/javascript/oracle/oj/ojdatacollection-common/PagingTableDataSource.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * 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.PagingTableDataSource
 * @classdesc Object representing data used by the paging component
 * @extends oj.TableDataSource
 * @implements oj.PagingModel
 * @param {Object} dataSource
 * @param {Object|null} options Array of options for the PagingControlDataSource
 * @constructor
 */
oj.PagingTableDataSource = function(dataSource, options)
{
  // Initialize
  options = options || {};
  
  if (!(dataSource instanceof oj.TableDataSource))
  {
    // we only support Array, oj.Collection, 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);
  }
  this.dataSource = dataSource;
  this._startIndex = 0;
  this._endIndex = 0;
  this.Init();
};

// Subclass from oj.DataSource 
oj.Object.createSubclass(oj.PagingTableDataSource, oj.TableDataSource, "oj.PagingTableDataSource");

/**
 * Initializes the instance.
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.Init = function()
{
  oj.PagingTableDataSource.superclass.Init.call(this);
};

/**
 * Retrieves the underlying DataSource.
 * @return {Object} the underlying oj.DataSource.
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.getWrappedDataSource = function()
{
    return this.dataSource;
};

/**
 * Get the current page
 * @return {number} The current page
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.getPage = function()
{
  if (this._fetchType == 'loadMore')
  {
    return 0;
  }
  return this._getPageFromStartIndex();
};

/**
 * Set the current page
 * @param {number} value The current page
 * @param {Object=} options Options<p>
 *                  pageSize: The page size.<p>
 * @return {Promise} promise object triggering done when complete..
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.setPage = function(value, options)
{
  options = options || {};
  value = parseInt(value, 10);
  
  try 
  {
    oj.PagingTableDataSource.superclass.handleEvent.call(this, oj.PagingModel.EventType['BEFOREPAGE'], {'page' : value, 'previousPage' : this._getPageFromStartIndex()});
  }
  catch (err)
  {
    return Promise.reject(null);
  }
  var previousPage = this._getPageFromStartIndex();
  this._pageSize = options['pageSize'] != null ? options['pageSize'] : this._pageSize;
  options['pageSize'] = this._pageSize;
  options['startIndex'] = value * this._pageSize;
  this._startIndex = options['startIndex'] == null ? this._startIndex : options['startIndex'];
  this._fetchType = 'page';
  var self = this;
  return new Promise(function(resolve, reject) 
  {
    // we only support paged fetches
    if (self._pageSize > 0)
    {
      self.dataSource.fetch(options).then(function(result)
      {
        result['startIndex'] = 0;
        self._updateEndIndex();
        oj.PagingTableDataSource.superclass.handleEvent.call(self, oj.PagingModel.EventType['PAGE'], {'page' : self._getPageFromStartIndex(), 'previousPage' : previousPage});
        resolve(null);
      },
      function(e)
      {
        self._startIndex = previousPage * self._pageSize;
        reject(null); 
      });
    }
    else
    {
      resolve(null);
    }
  });
};

/**
 * Get the current page start index
 * @return {number} The current page start index
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.getStartItemIndex = function()
{
  if (this._fetchType == 'loadMore')
  {
    return 0;
  }
  return this._startIndex;
};

/**
 * Get the current page end index
 * @return {number} The current page end index
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.getEndItemIndex = function()
{
  return this._endIndex;
};

/**
 * Get the page count
 * @return {number} The total number of pages
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.getPageCount = function()
{
  var totalSize = this.totalSize();
  return totalSize == -1 ? -1 : Math.ceil(totalSize / this._pageSize);
};

/**** start delegated functions ****/

/**
 * 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.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.at = function(index, options)
{
  return this.dataSource.at(index, options);
};

/**
 * 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.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.fetch = function(options)
{
  options = options || {};
  if (options['startIndex'] == null)
  {
    // this is just a refresh call from ojTable. So treat it as a paged fetch
    return this.setPage(this.getPage());
  }
  else
  {
    this._fetchType = 'loadMore';
  }
  this._startIndex = options['startIndex'] == null ? this._startIndex : options['startIndex'];
  var pageSize = options['pageSize'] != null ? options['pageSize'] : this._pageSize;
  options['pageSize'] = pageSize;
  options['startIndex'] = this._startIndex;
  
  var self = this;
  return new Promise(function(resolve, reject) 
  {
    // we only support paged fetches
    if (pageSize > 0)
    {
      self.dataSource.fetch(options).then(function(result)
      {
        self._updateEndIndex();
        resolve(result);
      },
      function(e)
      {
        reject(e);
      });
    }
    else
    {
      resolve(null);
    }
  });
};

/**
 * 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.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.get = function(id, options)
{
  return this.dataSource.get(id, options);
};

/**
 * Determines whether this TableDataSource 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
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.getCapability = function(feature)
{
  return this.dataSource.getCapability(feature);
};

/**
 * Attach an event handler to the datasource
 * @param {string} eventType eventType supported by the datasource
 * @param {function(Object)} eventHandler event handler function
 * @return {function(Object)} the event handler function attached to the event 
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.on = function(eventType, eventHandler)
{
  var self = this;
  var dataSource = (/** @type {{on: Function}} */ (this.dataSource));
  
  if (eventType == oj.TableDataSource.EventType['SYNC'])
  {
    var ev = function(event){self._handleSyncEvent(event, eventHandler);}
    dataSource.on(eventType, ev);
    return ev;
  }
  else if (eventType == oj.TableDataSource.EventType['ADD'] ||
           eventType == oj.TableDataSource.EventType['REMOVE'] ||
           eventType == oj.TableDataSource.EventType['CHANGE'])
  {
    var ev = function(event){self._handleRowEvent(event, eventHandler);}
    dataSource.on(eventType, ev);
    return ev;
  }
  else if (eventType == oj.TableDataSource.EventType['REFRESH'] ||
           eventType == oj.TableDataSource.EventType['RESET'])
  {
    var ev = function(event){
      self._startIndex = 0;
      eventHandler(event);
    }
    dataSource.on(eventType, ev);
    return ev;
  }
  else if (eventType == oj.PagingModel.EventType['PAGE'] ||
           eventType == oj.PagingModel.EventType['PAGECOUNT'])
  {
    oj.PagingTableDataSource.superclass.on.call(this, eventType, eventHandler);
    return eventHandler;
  }
  else
  {
    dataSource.on(eventType, eventHandler);
    return eventHandler;
  }
};

/**
 * Detach an event handler from the datasource
 * @param {string} eventType eventType supported by the datasource
 * @param {function(Object)} eventHandler event handler function
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.off = function(eventType, eventHandler)
{
  if (eventType == oj.PagingModel.EventType['PAGE'] ||
      eventType == oj.PagingModel.EventType['PAGECOUNT'])
  {
    oj.PagingTableDataSource.superclass.off.call(this, eventType, eventHandler);
    return eventHandler;
  }
  var dataSource = (/** @type {{off: Function}} */ (this.dataSource));
  return dataSource.off(eventType, eventHandler);
};

/**
 * Performs a sort on the data source.
 * @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.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.sort = function(criteria)
{
  return this.dataSource.sort(criteria);
};

/**
 * Return the total size of data available, including server side if not local.
 * @returns {number} total size of data
 * @export
 * @expose
 * @memberof! oj.PagingTableDataSource
 * @instance
 */
oj.PagingTableDataSource.prototype.totalSize = function()
{
  return this.dataSource.totalSize();
};

/**** end delegated functions ****/

oj.PagingTableDataSource.prototype._getPageFromStartIndex = function()
{
  if (this._pageSize > 0)
  {
    return Math.floor(this._startIndex / this._pageSize);
  }
  return 0;
}

oj.PagingTableDataSource.prototype._handleRowEvent = function(event, eventHandler)
{
  var ignoreRows = [];
  var i;
  for (i = 0; i < event['indexes'].length; i++)
  {
    var rowIdx = event['indexes'][i];
    if (rowIdx !== undefined) 
    {
      // adjust by startIndex
      rowIdx = rowIdx - this._startIndex;
      
      if (rowIdx < 0 || rowIdx >= this._startIndex + this._pageSize)
      {
        // ignore if it's not in our page range
        ignoreRows.push(i);
      }
    }
  }
  
  if (ignoreRows.length > 0)
  {
    ignoreRows.sort(function(a, b) 
    { 
      return a - b;
    });
    
    for (i = ignoreRows.length - 1; i >= 0; i--)
    {
      event['data'].splice(ignoreRows[i], 1);
      event['indexes'].splice(ignoreRows[i], 1);
      event['keys'].splice(ignoreRows[i], 1);
    }
  }
  
  this._updateEndIndex();
  
  event['startIndex'] = this._startIndex;
  eventHandler(event);
};

oj.PagingTableDataSource.prototype._handleSyncEvent = function(event, eventHandler)
{
  var eventStartIndex = event['startIndex'];
  
  if (eventStartIndex != this._startIndex)
  {
    this._startIndex = event['startIndex'];
  }
  this._updateEndIndex();
  
  if (this._fetchType == 'page')
  {
    // For paged fetches, reset the startIndex to zero since we always render
    // starting at 0
    var clonedEvent = {};
    oj.CollectionUtils.copyInto(clonedEvent, event);
    clonedEvent['startIndex'] = 0;
    eventHandler(clonedEvent);
  }
  else
  {
    eventHandler(event);
  }
};

oj.PagingTableDataSource.prototype._updateEndIndex = function()
{
  var totalSize = this.totalSize();
  this._endIndex = this._startIndex + this._pageSize - 1;
  
  if (totalSize > 0)
  {
    this._endIndex = this._endIndex > totalSize - 1 ? totalSize - 1 : this._endIndex;
  }
};

/**
 * @export
 * Event types
 * @enum {string}
 */
oj.PagingTableDataSource.EventType =
  {
    /** Triggered when a Row is added to a PagingDataSource */
    'ADD': "add",
    /** Triggered when a Row is removed from a PagingDataSource */
    'REMOVE': "remove",
    /** Triggered when a PagingDataSource is reset */
    'RESET': "reset",
    /** Triggered when a PagingDataSource has been updated by a fetch */
    'SYNC': "sync",
    /** Triggered when a PagingDataSource has been refreshed */
    'REFRESH': "refresh",
    /** Triggered when a PagingDataSource has been sorted */
    'SORT': "sort"
  };