UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

679 lines (655 loc) 17.9 kB
/** * @fileoverview * DataProvider is a special class to provide part of large amount of data to other widgets (e.g., DataTable). * For example, suppose a database table is filled with 1000 records, the provider can fetch 100 items from it * first, then the next 100 ones. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /localizations/ */ (function(){ "use strict"; /** * DataSet is a special class to provide large amount of data to other widgets (e.g., DataTable). * As transport large amount of data on internet is time cosuming, dataset may tranfer part of data at a time. * For example, suppose a database table is filled with 1000 records, the dataSet can fetch 100 items from it * first, then the next 100 ones in a secondary query. * BaseDataSet is the base class of all providers. * * @class * @augments ObjectEx * * @property {Bool} enableCache Whether previous fetched data can be cached for further use. * @property {Int} defaultTimeout Default timeout milliseconds when fetching data. 0 means never timeout. * @property {Array} sortFields Field names to sort data. If field name is prefixed with '!', means sort in desc order. * e.g. ['id', '!name']. */ /** * Invoked when record count in dataset is changed * event param of it has field: {totalCount} * @name Kekule.Widget.BaseDataSet#totalCountChange * @event */ /** * Invoked when data in dataset is changed * event param of it has field: {totalCount} * @name Kekule.Widget.BaseDataSet#dataChange * @event */ Kekule.Widget.BaseDataSet = Class.create(ObjectEx, /** @lends Kekule.Widget.BaseDataSet# */ { /** @private */ CLASS_NAME: 'Kekule.Widget.BaseDataSet', /** @private */ PREFIX_SORT_DESC: '!', /** @private */ initProperties: function() { this.defineProp('enableCache', {'dataType': DataType.BOOL, 'getter': function() { return this.getPropStoreFieldValue('enableCache') && this.getCacheAvailable(); }, 'setter': function(value) { this.setPropStoreFieldValue('enableCache', value); if (!value) this.clearCache(); } }); this.defineProp('defaultTimeout', {'dataType': DataType.INT}); this.defineProp('sortFields', { 'dataType': DataType.ARRAY, 'setter': function(value) { var a = value? Kekule.ArrayUtils.toArray(value): null; this.setPropStoreFieldValue('sortFields', a); this.sortFieldsChanged(a); } }); // private this.defineProp('cache', {'dataType': DataType.ARRAY}); }, /** @ignore */ initPropValues: function(/*$super*/) { this.tryApplySuper('initPropValues') /* $super() */; this.setEnableCache(true); this.setCache([]); this.setDefaultTimeout(20000); }, /** @ignore */ finalize: function(/*$super*/) { this.clearCache(); this.setCache(null); this.tryApplySuper('finalize') /* $super() */; }, /** * Notify the data in dataset has been changed. * @private */ dataChanged: function() { this.clearCache(); this.invokeEvent('dataChange'); }, /* @private */ /* getSortFieldInfo: function(sortFields) { var sortFieldInfos = []; for (var i = 0, l = sortFields.length; i < l; ++i) { var info = {}; var field = sortFields[i] || ''; if (field.startsWith(this.PREFIX_SORT_DESC)) // sort desc { info.field = field.substr(1); info.desc = true; } else { info.field = field; info.desc = false; } sortFieldInfos.push(info); } return sortFieldInfos; }, */ /** * Called when sort fields is changed. * @param {Array} newFields * @private */ sortFieldsChanged: function(newFields) { // usually sort field change caused cache to invalidate this.clearCache(); this.doSortFieldsChanged(newFields); }, /** * Do actual work of sorting data. * Descendants need to override this method. * @param {Array} newFields */ doSortFieldsChanged: function(newFields) { // do nothing here }, /** * Returns total data item length. * Returns -1 needs unknown. * @returns {Int} */ getTotalCount: function() { return this.doGetTotalCount() || 0; }, /** * Do actual work of get data item length. * Descendants should override this method. * @returns {Int} * @private */ doGetTotalCount: function() { // do nothing here }, /** * Notify total record count in dataset has been changed. * Descendants should call this method when necessary. * @private */ totalCountChanged: function(newCount) { this.invokeEvent('totalCountChange', {'totalCount': newCount}); }, /** * Returns the first index of data (usually 0). * Descendants may override this method. * @returns {Int} */ getFirstIndex: function() { return 0; }, /** * Returns the first index of data (usually total data count - 1). * Descendants may override this method. * @returns {Int} */ getLastIndex: function() { return (this.getTotalCount() || 0) - 1; }, /** * Returns whether this provider can use cache. * Descendants may override this method. * @returns {Bool} */ getCacheAvailable: function() { return true; }, /** * Remove all data in cache. */ clearCache: function() { this.getCache().length = 0; return this; }, /** * Save array of data to cache. * @param {Array} data * @param {Int} fromIndex Starting index in cache. * @param {Int} count Total data item count * @private */ saveCacheData: function(data, fromIndex) { //if (this.getEnableCache()) { var cache = this.getCache(); var currIndex = fromIndex; for (var i = 0; i < data.length; ++i) { cache[currIndex] = data[i] || null; ++currIndex; } } }, /** * Try load data from cache. If cache does not have all essential data, null will be returned. * @param {Int} fromIndex * @param {Int} count * @returns {Array} * @private */ loadCacheData: function(fromIndex, count) { if (!this.getEnableCache()) return null; else { var cache = this.getCache(); if (cache.length < fromIndex + count) return null; var result = []; for (var i = fromIndex, l = fromIndex + count; i < l; ++i) { var item = cache[i]; if (item === undefined) // not in cache return null; else result.push(item); } return result; } }, /** * Fetch data in a range. * @param {Hash} options Options may include the following fields: * { * fromIndex: from index of data, * count: count of data item to retrieve, 0 means retrieve all data, * ignoreCache: Force to not use cache even if enableCache property is true, * timeout: milliseconds, * callback: callback function called when data are successful retrieved, callback(dataArray). * errCallback: callback when error occurs callback(err) * timeOutCallback: callback when timeout. If this callback is not set, errCallback will be called instead. * } */ fetch: function(options) { var op = Object.extend({ 'fromIndex': 0, 'count': 0 //'timeout': this.getDefaultTimeout() }, options); op.timeout = op.timeout || this.getDefaultTimeout(); // try load from cache first if (this.getEnableCache()) { var data = this.loadCacheData(op.fromIndex, op.count); if (data) { op.callback(data); return; } } var self = this; var timeouted = false; var done = function(data) { if (timeouted) return; if (timeoutId) clearTimeout(timeoutId); if (options.callback) options.callback(data); if (self.getEnableCache()) // cache data { self.saveCacheData(data, op.fromIndex); } }; var error = function(err) { if (timeouted) return; if (timeoutId) clearTimeout(timeoutId); if (options.errCallback) options.errCallback(err); }; if (op.timeout > 0) { var timeoutCallback = function() { if (timeoutId) clearTimeout(timeoutId); //console.log(op.timeout, op.timeoutCallback, op.errCallback); timeouted = true; if (op.timeoutCallback) op.timeoutCallback(); else if (options.errCallback) options.errCallback(Kekule.$L('ErrorMsg.FETCH_DATA_TIMEOUT')); }; var timeoutId = setTimeout(timeoutCallback, op.timeout); } op.callback = done; op.errCallback = error; var result = this.doFetch(op.fromIndex, op.count, op.callback, op.errCallback); }, /** * Do actual work of fetch data. * Descendants should override this method. * @param {Int} fromIndex * @param {Int} count * @param {Func} callback * @param {Func} errCallback */ doFetch: function(fromIndex, count, callback, errCallback) { // fo nothing here } }); /** * A simple data set, stored all data in an internal array. * * @class * @augments Kekule.Widget.BaseDataSet * @param {Array} data * * @property {Array} data */ Kekule.Widget.ArrayDataSet = Class.create(Kekule.Widget.BaseDataSet, /** @lends Kekule.Widget.ArrayDataSet# */ { /** @private */ CLASS_NAME: 'Kekule.Widget.ArrayDataSet', /** @constructs */ initialize: function(/*$super, */data) { this.tryApplySuper('initialize') /* $super() */; this.setData(data); }, /** @private */ initProperties: function() { this.defineProp('data', {'dataType': DataType.ARRAY, 'setter': function(value) { this.setPropStoreFieldValue('data', value); // if has sort fields, sort data first var sortFields = this.getSortFields(); if (sortFields) this.doSortFieldsChanged(sortFields); this.dataChanged(); } }); }, /** @ignore */ getCacheAvailable: function() { // as all data are in data property, no need to cache. return false; }, /** @ignore */ doSortFieldsChanged: function(newFields) { /* var sortFieldInfos = this.getSortFieldInfo(newFields); var sortFunc = function(hash1, hash2) { var compareValue = 0; for (var i = 0, l = sortFieldInfos.length; i < l; ++i) { var field = sortFieldInfos[i].field; var v1 = hash1[field] || ''; var v2 = hash2[field] || ''; compareValue = (v1 > v2)? 1: (v1 < v2)? -1: 0; if (sortFieldInfos[i].desc) compareValue = -compareValue; if (compareValue !== 0) break; } return compareValue; }; var data = this.getData() || []; data.sort(sortFunc); this.dataChanged(); return this; */ var data = this.getData() || []; Kekule.ArrayUtils.sortHashArray(data, newFields); this.dataChanged(); return this; }, /** @ignore */ doGetTotalCount: function() { return (this.getData() || []).length; }, /** @ignore */ doFetch: function(fromIndex, count, callback, errCallback) { var result = []; var data = this.getData() || []; for (var i = fromIndex, l = Math.min(fromIndex + count, data.length); i < l; ++i) { var item = data[i]; result.push(item); } // debug /* var done = function() { //if ((fromIndex / count) % 2) callback(result); //else // errCallback('A Error'); }; setTimeout(done, 500); */ callback(result); } }); /** * A class to divide data from dataSet to multiple pages, * should be used together with data table widget. * @class * @augments ObjectEx * @param {Kekule.Widget.BaseDataSet} dataSet * * @property {Kekule.Widget.BaseDataSet} dataSet The data provider to fetch data * @property {Int} pageSize Item count in one page. * @property {Int} currPageIndex Index of current page. * @property {Array} currPageData Cache data of current page. * @property {Array} sortFields Field names to sort data in dataset. */ /** * Invoked when data of new page is starting to retrieve. * event param of it has field: {pageIndex} * @name Kekule.Widget.DataPager#pageRetrieve * @event */ /** * Invoked when page index is changed and data are successfully retrieved. * event param of it has field: {pageIndex, data} * @name Kekule.Widget.DataPager#dataFetched * @event */ /** * Invoked when retrieving data error. * event param of it has field: {err} * @name Kekule.Widget.DataPager#dataError * @event */ /** * Invoked when page count is changed * event param of it has field: {pageCount} * @name Kekule.Widget.DataPager#pageCountChange * @event */ Kekule.Widget.DataPager = Class.create(ObjectEx, /** @lends Kekule.Widget.DataPager# */ { /** @private */ CLASS_NAME: 'Kekule.Widget.DataPager', /** @constructs */ initialize: function(/*$super, */dataSet) { this.tryApplySuper('initialize') /* $super() */; this.setPropStoreFieldValue('pageSize', 10); // default value this.setDataSet(dataSet); }, /** @private */ initProperties: function() { this.defineProp('dataSet', {'dataType': 'Kekule.Widget.BaseDataSet', 'setter': function(value) { var old = this.getDataSet(); if (value !== old) { this.setPropStoreFieldValue('dataSet', value); this.dataSetChange(old, value); this.switchToPage(this.getCurrPageIndex() || 0); } } }); this.defineProp('pageSize', {'dataType': DataType.INT, 'setter': function(value) { if (value !== this.getPageSize()) // page size change, need to reload data in cache { this.setPropStoreFieldValue('pageSize', value); this.pageCountChanged(this.getPageCount()); this.switchToPage(this.getCurrPageIndex() || 0); } } }); this.defineProp('currPageIndex', {'dataType': DataType.INT}); this.defineProp('currPageData', {'dataType': DataType.ARRAY}); this.defineProp('sortFields', { 'dataType': DataType.ARRAY, 'serializable': false, 'getter': function() { return this.getDataSet() && this.getDataSet().getSortFields(); }, 'setter': function(value) { if (this.getDataSet()) { this.getDataSet().setSortFields(value); //this.sortFieldsChanged(); } } }); }, /** @ignore */ initPropValues: function(/*$super*/) { this.tryApplySuper('initPropValues') /* $super() */; this.setCurrPageIndex(0); }, /** @private */ dataSetChange: function(oldDataSet, newDataSet) { if (oldDataSet) { oldDataSet.RemoveEventListener('dataChange', this.reactDataSetDataChange, this); oldDataSet.RemoveEventListener('totalCountChange', this.reactDataSetTotalCountChange, this); } if (newDataSet) { newDataSet.addEventListener('dataChange', this.reactDataSetDataChange, this); newDataSet.addEventListener('totalCountChange', this.reactDataSetTotalCountChange, this); } this.pageCountChanged(this.getPageCount()); }, /** @private */ reactDataSetDataChange: function(e) { this.dataChanged(); }, /** * Called when data in dataset has been changed, need to refetch data. * @private */ dataChanged: function() { this.pageCountChanged(this.getPageCount()); this.switchToPage(this.getCurrPageIndex() || 0); }, /** @private */ reactDataSetTotalCountChange: function(e) { this.pageCountChanged(this.getPageCount()); }, /* @private */ /* sortFieldsChanged: function() { //this.switchToPage(this.getCurrPageIndex() || 0); }, */ /** * Returns total page count. * @returns {Int} */ getPageCount: function() { var dataset = this.getDataSet(); var totalCount = dataset? (dataset.getTotalCount() || 0): 0; return Math.ceil(totalCount / this.getPageSize()); }, /** * Notify page count has been changed. * @private */ pageCountChanged: function(newPageCount) { this.invokeEvent('pageCountChange', {'pageCount': newPageCount}); }, /** * Returns data in current page. * @param {Hash} options Options may include the following fields: * { * pageIndex: index of data page. If not set, currPageIndex will be used, * ignoreCache: Force to not use cache even if enableCache property is true, * timeout: milliseconds, * callback: callback function called when data are successful retrieved, callback(dataArray). * errCallback: callback when error occurs callback(err) * timeOutCallback: callback when timeout. If this callback is not set, errCallback will be called instead. * } * @private */ fetchPageData: function(options) { var pageSize = this.getPageSize(); var pageIndex = Kekule.ObjUtils.isUnset(options.pageIndex)? this.getCurrPageIndex(): options.pageIndex; var fromIndex = pageSize * pageIndex; var count = this.getPageSize(); var ops = Object.create(options); Object.extend(ops, {'fromIndex': fromIndex, 'count': count}); this.getDataSet().fetch(ops); return this; }, /** * Request data on page index. * When data is retrieved, event pageSwitched will be invoked. * When error or timeout, event pageSwitchError will be invoked. * @param {Int} pageIndex * @param {Int} timeout */ switchToPage: function(pageIndex, timeout) { var self = this; var ops = { 'pageIndex': pageIndex, 'timeout': timeout, 'callback': function(data) { self.setCurrPageData(data); self.setCurrPageIndex(pageIndex); self.invokeEvent('dataFetched', {'pageIndex': pageIndex, 'data': data}); }, 'errCallback': function(err) { self.invokeEvent('dataError', {'error': err}); Kekule.error(err); // TODO: need to invoke an exception here? } }; this.invokeEvent('pageRetrieve', {'pageIndex': pageIndex}); this.fetchPageData(ops); } }); })();