define([
|
'./List',
|
'./_StoreMixin',
|
'dojo/_base/declare',
|
'dojo/_base/lang',
|
'dojo/dom-construct',
|
'dojo/on',
|
'dojo/when',
|
'dojo/query',
|
'./util/misc'
|
], function (List, _StoreMixin, declare, lang, domConstruct, on, when, query, miscUtil) {
|
|
var preloadId = 0;
|
|
function nextPreloadId() {
|
return preloadId++;
|
}
|
|
function isRowNode(node) {
|
return node && (node.className.indexOf('dgrid-row') >= 0 ||
|
node.className.indexOf('dgrid-loading') >= 0);
|
}
|
|
function isPreloadNode(node) {
|
return node && node.className.indexOf('dgrid-preload') >= 0;
|
}
|
|
return declare([List, _StoreMixin], {
|
// summary:
|
// Extends List to include virtual scrolling functionality, querying a
|
// dojo/store instance for the appropriate range when the user scrolls.
|
|
// minRowsPerPage: Integer
|
// The minimum number of rows to request at one time.
|
minRowsPerPage: 25,
|
|
// maxRowsPerPage: Integer
|
// The maximum number of rows to request at one time.
|
maxRowsPerPage: 250,
|
|
// maxEmptySpace: Integer
|
// Defines the maximum size (in pixels) of unrendered space below the
|
// currently-rendered rows. Setting this to less than Infinity can be useful if you
|
// wish to limit the initial vertical scrolling of the grid so that the scrolling is
|
// not excessively sensitive. With very large grids of data this may make scrolling
|
// easier to use, albiet it can limit the ability to instantly scroll to the end.
|
maxEmptySpace: Infinity,
|
|
// bufferRows: Integer
|
// The number of rows to keep ready on each side of the viewport area so that the user can
|
// perform local scrolling without seeing the grid being built. Increasing this number can
|
// improve perceived performance when the data is being retrieved over a slow network.
|
bufferRows: 10,
|
|
// farOffRemoval: Integer
|
// Defines the minimum distance (in pixels) from the visible viewport area
|
// rows must be in order to be removed. Setting to Infinity causes rows
|
// to never be removed.
|
farOffRemoval: 2000,
|
|
// queryRowsOverlap: Integer
|
// Indicates the number of rows to overlap queries. This helps keep
|
// continuous data when underlying data changes (and thus pages don't
|
// exactly align)
|
queryRowsOverlap: 0,
|
|
// pagingMethod: String
|
// Method (from dgrid/util/misc) to use to either throttle or debounce
|
// requests. Default is "debounce" which will cause the grid to wait until
|
// the user pauses scrolling before firing any requests; can be set to
|
// "throttleDelayed" instead to progressively request as the user scrolls,
|
// which generally incurs more overhead but might appear more responsive.
|
pagingMethod: 'debounce',
|
|
// pagingDelay: Integer
|
// Indicates the delay (in milliseconds) imposed upon pagingMethod, to wait
|
// before paging in more data on scroll events. This can be increased to
|
// reduce client-side overhead or the number of requests sent to a server.
|
pagingDelay: miscUtil.defaultDelay,
|
|
// keepScrollPosition: Boolean
|
// When refreshing the list, controls whether the scroll position is
|
// preserved, or reset to the top. This can also be overridden for
|
// specific calls to refresh.
|
keepScrollPosition: false,
|
|
// rowHeight: Number
|
// Average row height, computed in renderQuery during the rendering of
|
// the first range of data.
|
rowHeight: 0,
|
|
// _deleteQueue: Array
|
// List of DOM nodes queued for deletion.
|
_deleteQueue: [],
|
|
postCreate: function () {
|
this.inherited(arguments);
|
var self = this;
|
// check visibility on scroll events
|
on(this.bodyNode, 'scroll',
|
miscUtil[this.pagingMethod](function (event) {
|
self._processScroll(event);
|
}, null, this.pagingDelay)
|
);
|
},
|
|
renderQuery: function (query, options) {
|
// summary:
|
// Creates a preload node for rendering a query into, and executes the query
|
// for the first page of data. Subsequent data will be downloaded as it comes
|
// into view.
|
// query: Function
|
// Function to be called when requesting new data.
|
// options: Object?
|
// Optional object containing the following:
|
// * container: Container to build preload nodes within; defaults to this.contentNode
|
|
var self = this,
|
container = (options && options.container) || this.contentNode,
|
preload,
|
topPreloadNode, preloadNode,
|
queryLevel,
|
preloadLevel = 0,
|
start = (options && options.start) || 0;
|
|
if ('level' in query) {
|
preloadLevel = queryLevel = query.level;
|
}
|
|
preload = {
|
query: query,
|
count: 0,
|
level: preloadLevel,
|
top: false
|
};
|
|
// Initial query; set up top and bottom preload nodes
|
var topPreload = {
|
node: domConstruct.create('div', {
|
className: 'dgrid-preload',
|
style: { height: '0' }
|
}, container),
|
count: 0,
|
query: query,
|
next: preload,
|
level: preloadLevel,
|
top: true
|
};
|
topPreloadNode = topPreload.node;
|
topPreloadNode.rowIndex = 0;
|
preload.previous = topPreload;
|
|
preloadNode = preload.node = domConstruct.create('div', {
|
className: 'dgrid-preload',
|
style: { height: '0' }
|
}, container);
|
|
// Add preload ids.
|
topPreload.id = nextPreloadId();
|
topPreloadNode.setAttribute('data-preloadid', topPreload.id);
|
preload.id = nextPreloadId();
|
preloadNode.setAttribute('data-preloadid', preload.id);
|
|
// this preload node is used to represent the area of the grid that hasn't been
|
// downloaded yet
|
preloadNode.rowIndex = this.minRowsPerPage;
|
|
self._insertPreload(topPreload);
|
|
var loadingNode = domConstruct.create('div', {
|
className: 'dgrid-loading'
|
}, preloadNode, 'before'),
|
innerNode = domConstruct.create('div', {
|
className: 'dgrid-below'
|
}, loadingNode);
|
innerNode.innerHTML = this.loadingMessage;
|
|
// Establish query options, mixing in our own
|
options = lang.mixin({ start: 0, count: this.minRowsPerPage }, options);
|
if (queryLevel != null) {
|
options.queryLevel = queryLevel;
|
}
|
|
// Protect the query within a _trackError call, but return the resulting collection
|
return this._trackError(function () {
|
var results = query(options);
|
|
// Render the result set
|
return self.renderQueryResults(results, preloadNode, options).then(function (trs) {
|
return results.totalLength.then(function (total) {
|
var trCount = trs.length;
|
var parentNode = preloadNode.parentNode;
|
|
if (self._rows && !('queryLevel' in options)) {
|
self._rows.min = 0;
|
self._rows.max = trCount === total ? Infinity : trCount - 1;
|
}
|
|
domConstruct.destroy(loadingNode);
|
if (!('queryLevel' in options)) {
|
self._total = total;
|
}
|
// now we need to adjust the height and total count based on the first result set
|
if (total === 0 && parentNode) {
|
if (self.noDataNode) {
|
domConstruct.destroy(self.noDataNode);
|
}
|
self._insertNoDataNode(parentNode);
|
}
|
|
topPreload.count = start;
|
preload.count = total - trCount - start;
|
preloadNode.rowIndex = start + trCount;
|
|
if (total) {
|
self._updatePreloadRowHeights(topPreload);
|
} else {
|
preloadNode.style.display = 'none';
|
topPreloadNode.style.display = 'none';
|
}
|
|
if (self._previousScrollPosition && parentNode.offsetHeight) {
|
// Restore position after a refresh operation w/ keepScrollPosition but only
|
// if the rows have been inserted into the DOM.
|
self.scrollTo(self._previousScrollPosition);
|
delete self._previousScrollPosition;
|
}
|
|
// Redo scroll processing in case the query didn't fill the screen,
|
// or in case scroll position was restored
|
return when(self._processScroll()).then(function () {
|
return trs;
|
});
|
});
|
}).otherwise(function (err) {
|
// remove the loadingNode and re-throw
|
domConstruct.destroy(loadingNode);
|
throw err;
|
});
|
});
|
},
|
|
_insertPreload: function (newTopPreload) {
|
var preload = this.preload;
|
if (!preload) {
|
// first one
|
this.preload = newTopPreload;
|
return;
|
}
|
|
while (preload.node.compareDocumentPosition(newTopPreload.node) & Node.DOCUMENT_POSITION_PRECEDING) {
|
preload = preload.previous;
|
if (preload == null) {
|
return;
|
}
|
}
|
|
while (preload.node.compareDocumentPosition(newTopPreload.node) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
if (!preload.next) {
|
break;
|
}
|
preload = preload.next;
|
}
|
// insert, newPreload before preload
|
preload.previous.next = newTopPreload;
|
newTopPreload.previous = preload.previous;
|
var newBottomPreload = newTopPreload.next;
|
newBottomPreload.next = preload;
|
preload.previous = newBottomPreload;
|
},
|
|
refresh: function (options) {
|
// summary:
|
// Refreshes the contents of the grid.
|
// options: Object?
|
// Optional object, supporting the following parameters:
|
// * keepScrollPosition: like the keepScrollPosition instance property;
|
// specifying it in the options here will override the instance
|
// property's value for this specific refresh call only.
|
|
var self = this,
|
keep = (options && options.keepScrollPosition);
|
|
// Fall back to instance property if option is not defined
|
if (typeof keep === 'undefined') {
|
keep = this.keepScrollPosition;
|
}
|
|
// Store scroll position to be restored after new total is received
|
if (keep) {
|
this._previousScrollPosition = this.getScrollPosition();
|
}
|
|
this.inherited(arguments);
|
if (this._renderedCollection) {
|
// render the query
|
|
// renderQuery calls _trackError internally
|
return this.renderQuery(function (queryOptions) {
|
return self._renderedCollection.fetchRange({
|
start: queryOptions.start,
|
end: queryOptions.start + queryOptions.count
|
});
|
}).then(function () {
|
self._emitRefreshComplete();
|
});
|
}
|
},
|
|
resize: function () {
|
this.inherited(arguments);
|
this._processScroll();
|
},
|
|
cleanup: function () {
|
this.inherited(arguments);
|
this.preload = null;
|
},
|
|
renderQueryResults: function (results) {
|
var rows = this.inherited(arguments);
|
var collection = this._getRenderedCollection(this.preload);
|
|
if (collection && collection.releaseRange) {
|
rows.then(function (resolvedRows) {
|
if (resolvedRows[0] && !resolvedRows[0].parentNode.tagName) {
|
// Release this range, since it was never actually rendered;
|
// need to wait until totalLength promise resolves, since
|
// Trackable only adds the range then to begin with
|
results.totalLength.then(function () {
|
collection.releaseRange(resolvedRows[0].rowIndex,
|
resolvedRows[resolvedRows.length - 1].rowIndex + 1);
|
});
|
}
|
});
|
}
|
|
return rows;
|
},
|
|
_getFirstRowSibling: function (container) {
|
// summary:
|
// Returns the DOM node that a new row should be inserted before
|
// when there are no other rows in the current result set.
|
// In the case of OnDemandList, this will always be the last child
|
// of the container (which will be a trailing preload node).
|
return container.lastChild;
|
},
|
|
_calcRowHeight: function (rowElement) {
|
// summary:
|
// Calculate the height of a row. This is a method so it can be overriden for
|
// plugins that add connected elements to a row, like the tree
|
|
var sibling = rowElement.nextSibling;
|
|
// If a next row exists, compare the top of this row with the
|
// next one (in case "rows" are actually rendering side-by-side).
|
// If no next row exists, this is either the last or only row,
|
// in which case we count its own height.
|
if (sibling && !/\bdgrid-preload\b/.test(sibling.className)) {
|
return sibling.offsetTop - rowElement.offsetTop;
|
}
|
|
return rowElement.offsetHeight;
|
},
|
|
_calcAverageRowHeight: function (rowElements) {
|
// summary:
|
// Sets this.rowHeight based on the average from heights of the provided row elements.
|
|
var count = rowElements.length;
|
var height = 0;
|
for (var i = 0; i < count; i++) {
|
height += this._calcRowHeight(rowElements[i]);
|
}
|
// only update rowHeight if elements were passed and are in flow
|
if (count && height) {
|
return height / count;
|
} else {
|
return 0;
|
}
|
},
|
|
_updatePreloadRowHeights: function () {
|
var preload = this.preload;
|
if (!preload) {
|
return;
|
}
|
while (preload.previous) {
|
preload = preload.previous;
|
}
|
while (preload) {
|
if (!preload.rowHeight) {
|
preload.rowHeight = this.rowHeight ||
|
this._calcAverageRowHeight(preload.node.parentNode.querySelectorAll('.dgrid-row'));
|
this._adjustPreloadHeight(preload);
|
}
|
preload = preload.next;
|
}
|
},
|
|
lastScrollTop: 0,
|
_processScroll: function (evt) {
|
// summary:x
|
// Checks to make sure that everything in the viewable area has been
|
// downloaded, and triggering a request for the necessary data when needed.
|
var preload = this.preload,
|
rowHeight;
|
|
this._updatePreloadRowHeights();
|
rowHeight = preload && preload.rowHeight;
|
if (!rowHeight) {
|
return;
|
}
|
|
var grid = this,
|
scrollNode = grid.bodyNode,
|
// grab current visible top from event if provided, otherwise from node
|
visibleTop = (evt && evt.scrollTop) || this.getScrollPosition().y,
|
visibleBottom = scrollNode.offsetHeight + visibleTop,
|
priorPreload, preloadNode,
|
lastScrollTop = grid.lastScrollTop,
|
requestBuffer = grid.bufferRows * rowHeight,
|
searchBuffer = requestBuffer - rowHeight, // Avoid rounding causing multiple queries
|
// References related to emitting dgrid-refresh-complete if applicable
|
lastRows,
|
preloadSearchNext = true;
|
|
// XXX: I do not know why this happens.
|
// munging the actual location of the viewport relative to the preload node by a few pixels in either
|
// direction is necessary because at least WebKit on Windows seems to have an error that causes it to
|
// not quite get the entire element being focused in the viewport during keyboard navigation,
|
// which means it becomes impossible to load more data using keyboard navigation because there is
|
// no more data to scroll to to trigger the fetch.
|
// 1 is arbitrary and just gets it to work correctly with our current test cases; don’t wanna go
|
// crazy and set it to a big number without understanding more about what is going on.
|
// wondering if it has to do with border-box or something, but changing the border widths does not
|
// seem to make it break more or less, so I do not know…
|
var mungeAmount = 1;
|
|
grid.lastScrollTop = visibleTop;
|
|
function calculateDistanceOffset(preload, removeBelow) {
|
if (removeBelow) {
|
return preload.node.offsetTop - visibleBottom;
|
} else {
|
return visibleTop - (preload.node.offsetTop + preload.node.offsetHeight);
|
}
|
}
|
|
function traverseToEndPreload(preload, removeBelow) {
|
var direction = removeBelow ? 'next' : 'previous';
|
var nextPreload;
|
while ((nextPreload = preload[direction])) {
|
preload = nextPreload;
|
}
|
return preload;
|
}
|
|
function removeDistantNodes(preload, removeBelow) {
|
// we check to see the the nodes are "far off"
|
|
var startingPreload = preload;
|
preload = traverseToEndPreload(preload, removeBelow);
|
|
var distanceOff = calculateDistanceOffset(preload, removeBelow);
|
var farOffRemoval = grid.farOffRemoval;
|
var preloadNode = preload.node;
|
var domTraversal = removeBelow ? 'previousSibling' : 'nextSibling';
|
var count = 0;
|
var reclaimedHeight = 0;
|
var firstRowIndex;
|
var lastRowIndex;
|
|
function findNextPreload() {
|
var topPreloadWanted = !removeBelow;
|
var newPreload = preload;
|
while ((newPreload = newPreload[removeBelow ? 'previous' : 'next'])) {
|
if (topPreloadWanted === newPreload.top) {
|
return newPreload;
|
}
|
}
|
}
|
|
function isEmpty(aPreload) {
|
return isPreloadNode(aPreload.top ? aPreload.node.nextSibling : aPreload.node.previousSibling);
|
}
|
|
function traversePreload() {
|
var newPreload = findNextPreload();
|
var node;
|
|
if (newPreload && startingPreload !== newPreload && !isEmpty(newPreload)) {
|
adjustPreloadStats();
|
|
preload = newPreload;
|
preloadNode = preload.node;
|
distanceOff = calculateDistanceOffset(preload, removeBelow);
|
node = traverseNode(preloadNode);
|
resetRowIndexes(node);
|
|
return node;
|
}
|
}
|
|
function traverseNode(referenceNode) {
|
// Preload node referenced was first moved to the appropriate end of the list and
|
// now we are moving toward the viewable area.
|
var refIsPreload = isPreloadNode(referenceNode);
|
var node = referenceNode[domTraversal];
|
var childNode;
|
if (node) {
|
if (!isRowNode(node)) {
|
if (refIsPreload && isPreloadNode(node)) {
|
node = null;
|
} else {
|
childNode = traversePreload();
|
if (childNode) {
|
node = childNode;
|
} else {
|
node = traverseNode(node);
|
}
|
}
|
}
|
}
|
return node;
|
}
|
|
function adjustPreloadStats() {
|
// adjust the preloadNode based on the reclaimed space
|
preload.count += count;
|
if (removeBelow) {
|
preloadNode.rowIndex -= count;
|
}
|
grid._adjustPreloadHeight(preload);
|
count = 0;
|
|
grid._releaseRange(preload, removeBelow, firstRowIndex, lastRowIndex);
|
}
|
|
function resetRowIndexes(row) {
|
firstRowIndex = row && row.rowIndex;
|
lastRowIndex = undefined;
|
}
|
|
if (distanceOff > 2 * farOffRemoval) {
|
// there is a preloadNode that is far off;
|
// remove rows until we get to in the current viewport
|
var row;
|
var nextRow = traverseNode(preloadNode);
|
resetRowIndexes(nextRow);
|
|
while ((row = nextRow) && startingPreload !== preload) {
|
var currentRowHeight = grid._calcRowHeight(row);
|
if (reclaimedHeight + currentRowHeight + farOffRemoval > distanceOff || !isRowNode(row)) {
|
// we have reclaimed enough rows or we have gone beyond grid rows
|
nextRow = traversePreload();
|
continue;
|
}
|
|
reclaimedHeight += currentRowHeight;
|
count += row.count || 1;
|
grid._pruneRow(row, removeBelow);
|
|
if ('rowIndex' in row) {
|
lastRowIndex = row.rowIndex;
|
}
|
nextRow = traverseNode(row);
|
}
|
|
adjustPreloadStats();
|
grid._deleteNodeQueue();
|
}
|
}
|
|
function traversePreload(preload, moveNext) {
|
// Skip past preloads that are not currently connected
|
do {
|
preload = moveNext ? preload.next : preload.previous;
|
} while (preload && !preload.node.offsetWidth);
|
return preload;
|
}
|
|
while (preload && !preload.node.offsetWidth) {
|
// skip past preloads that are not currently connected
|
preload = preload.previous;
|
}
|
// there can be multiple preloadNodes (if they split, or multiple queries are created),
|
// so we can traverse them until we find whatever is in the current viewport, making
|
// sure we don't backtrack
|
while (preload && preload !== priorPreload) {
|
priorPreload = grid.preload;
|
grid.preload = preload;
|
preloadNode = preload.node;
|
var preloadTop = preloadNode.offsetTop;
|
|
if (visibleBottom + mungeAmount + searchBuffer < preloadTop) {
|
// the preload is below the line of sight
|
preload = traversePreload(preload, (preloadSearchNext = false));
|
}
|
else if (visibleTop - mungeAmount - searchBuffer > preloadTop + preloadNode.offsetHeight) {
|
// the preload is above the line of sight
|
preload = traversePreload(preload, (preloadSearchNext = true));
|
}
|
else {
|
// the preload node is visible, or close to visible, better show it
|
var offset = ((preloadNode.top ? visibleTop - requestBuffer :
|
visibleBottom) - preloadTop) / preload.rowHeight;
|
var count = (visibleBottom - visibleTop + 2 * requestBuffer) / preload.rowHeight;
|
// utilize momentum for predictions
|
var momentum = Math.max(
|
Math.min((visibleTop - lastScrollTop) * preload.rowHeight, grid.maxRowsPerPage / 2),
|
grid.maxRowsPerPage / -2);
|
count += Math.min(Math.abs(momentum), 10);
|
if (preloadNode.top) {
|
// at the top, adjust from bottom to top
|
offset -= count;
|
}
|
offset = Math.max(offset, 0);
|
if (offset < 10 && offset > 0 && count + offset < grid.maxRowsPerPage) {
|
// connect to the top of the preloadNode if possible to avoid excessive adjustments
|
count += Math.max(0, offset);
|
offset = 0;
|
}
|
count = Math.min(Math.max(count, grid.minRowsPerPage),
|
grid.maxRowsPerPage, preload.count);
|
|
if (count === 0) {
|
preload = traversePreload(preload, preloadSearchNext);
|
continue;
|
}
|
|
count = Math.ceil(count);
|
offset = Math.min(Math.floor(offset), preload.count - count);
|
|
var options = {};
|
preload.count -= count;
|
var beforeNode = preloadNode,
|
keepScrollTo,
|
queryRowsOverlap = grid.queryRowsOverlap,
|
bottomPreload = !preload.top && preload;
|
if (bottomPreload) {
|
// add new rows below
|
var previous = preload.previous;
|
if (previous) {
|
removeDistantNodes(preload);
|
if (offset > 0 && isPreloadNode(preloadNode.previousSibling)) {
|
// all of the nodes above were removed
|
offset = Math.min(preload.count, offset);
|
preload.previous.count += offset;
|
grid._adjustPreloadHeight(preload.previous, true);
|
preloadNode.rowIndex += offset;
|
queryRowsOverlap = 0;
|
}
|
else {
|
count += offset;
|
}
|
preload.count -= offset;
|
}
|
options.start = preloadNode.rowIndex - queryRowsOverlap;
|
options.count = Math.min(count + queryRowsOverlap, grid.maxRowsPerPage);
|
preloadNode.rowIndex = options.start + options.count;
|
}
|
else {
|
// add new rows above
|
if (preload.next) {
|
// remove out of sight nodes first
|
beforeNode = preloadNode.nextSibling;
|
removeDistantNodes(preload, true);
|
if (isPreloadNode(preloadNode.nextSibling)) {
|
// all of the nodes were removed, can position wherever we want
|
preload.next.count += preload.count - offset;
|
preload.next.node.rowIndex = offset + count;
|
grid._adjustPreloadHeight(preload.next);
|
preload.count = offset;
|
queryRowsOverlap = 0;
|
beforeNode = preload.next.node;
|
}
|
else {
|
keepScrollTo = true;
|
}
|
}
|
options.start = preload.count;
|
options.count = Math.min(count + queryRowsOverlap, grid.maxRowsPerPage);
|
options.scrollingUp = true;
|
}
|
if (keepScrollTo && beforeNode && beforeNode.offsetWidth) {
|
// Before adjusting the size of the preload node for the new rows yet to be loaded, remember
|
// the current position of beforeNode so the scroll position can be adjusted after
|
// the new rows are added.
|
keepScrollTo = beforeNode.offsetTop;
|
}
|
grid._adjustPreloadHeight(preload);
|
|
// use the query associated with the preload node to get the next "page"
|
if ('level' in preload.query) {
|
options.queryLevel = preload.query.level;
|
}
|
|
// Avoid spurious queries (ideally this should be unnecessary...)
|
if (!('queryLevel' in options) && (options.start > grid._total || options.count < 0)) {
|
continue;
|
}
|
|
// create a loading node as a placeholder while the data is loaded
|
var loadingNode = domConstruct.create('div', {
|
className: 'dgrid-loading',
|
style: { height: count * preload.rowHeight + 'px' }
|
}, beforeNode, 'before');
|
domConstruct.create('div', {
|
className: 'dgrid-' + (bottomPreload ? 'below' : 'above'),
|
innerHTML: grid.loadingMessage
|
}, loadingNode);
|
loadingNode.count = count;
|
|
// Query now to fill in these rows.
|
grid._trackError(function () {
|
// Use function to isolate the variables in case we make multiple requests
|
// (which can happen if we need to render on both sides of an island of already-rendered rows)
|
(function (loadingNode, below, keepScrollTo) {
|
/* jshint maxlen: 122 */
|
var rangeResults = preload.query(options);
|
lastRows = grid.renderQueryResults(rangeResults, loadingNode, options).then(function (rows) {
|
var gridRows = grid._rows;
|
if (gridRows && !('queryLevel' in options) && rows.length) {
|
// Update relevant observed range for top-level items
|
if (below) {
|
if (gridRows.max <= gridRows.min) {
|
// All rows were removed; update start of rendered range as well
|
gridRows.min = rows[0].rowIndex;
|
}
|
gridRows.max = rows[rows.length - 1].rowIndex;
|
}
|
else {
|
if (gridRows.max <= gridRows.min) {
|
// All rows were removed; update end of rendered range as well
|
gridRows.max = rows[rows.length - 1].rowIndex;
|
}
|
gridRows.min = rows[0].rowIndex;
|
}
|
}
|
|
// can remove the loading node now
|
beforeNode = loadingNode.nextSibling;
|
domConstruct.destroy(loadingNode);
|
// beforeNode may have been removed if the query results loading node was removed
|
// as a distant node before rendering
|
if (keepScrollTo && beforeNode && beforeNode.offsetWidth) {
|
// if the preload area above the nodes is approximated based on average
|
// row height, we may need to adjust the scroll once they are filled in
|
// so we don't "jump" in the scrolling position
|
grid.scrollTo({
|
y: grid.bodyNode.scrollTop + beforeNode.offsetTop - keepScrollTo
|
});
|
}
|
|
rangeResults.totalLength.then(function (total) {
|
if (!('queryLevel' in options)) {
|
grid._total = total;
|
if (grid._rows && grid._rows.max >= grid._total - 1) {
|
grid._rows.max = Infinity;
|
}
|
}
|
if (below) {
|
// if it is below, we will use the total from the collection to update
|
// the count of the last preload in case the total changes as
|
// later pages are retrieved
|
|
// recalculate the count
|
below.count = total - below.node.rowIndex;
|
// readjust the height
|
grid._adjustPreloadHeight(below);
|
}
|
});
|
|
// make sure we have covered the visible area
|
grid._processScroll();
|
return rows;
|
}, function (e) {
|
domConstruct.destroy(loadingNode);
|
throw e;
|
});
|
})(loadingNode, bottomPreload, keepScrollTo);
|
});
|
|
preload = preload.previous;
|
|
}
|
}
|
|
// return the promise from the last render
|
return lastRows;
|
},
|
|
_adjustPreloadHeight: function (preload, noMax) {
|
preload.node.style.height = this._calculatePreloadHeight(preload, noMax) + 'px';
|
},
|
|
_calculatePreloadHeight: function (preload, noMax) {
|
return Math.min(preload.count * preload.rowHeight,
|
noMax ? Infinity : this.maxEmptySpace);
|
},
|
|
_pruneRow: function (rowElement, removeBelow, options) {
|
// Calling _pruneRow indicates the row is not being deleted permanantly but could be restored
|
// as the grid scrolls.
|
|
// Just do cleanup here, as we will do a more efficient node destruction will be done later.
|
this.removeRow(rowElement, true, options);
|
this._queueNodeForDeletion(rowElement);
|
},
|
|
_queueNodeForDeletion: function (node) {
|
this._deleteQueue.push(node);
|
},
|
|
_deleteNodeQueue: function () {
|
var trashBin = document.createElement('div');
|
var toDelete = this._deleteQueue;
|
for (var i = toDelete.length; i--;) {
|
trashBin.appendChild(toDelete[i]);
|
}
|
this._deleteQueue = [];
|
setTimeout(function () {
|
// we can defer the destruction until later
|
domConstruct.destroy(trashBin);
|
}, 1);
|
},
|
|
_removePreloads: function (preloadNodes) {
|
// summary:
|
// Remove the preload objects from the linked list that correspond to the
|
// supplied DOM nodes.
|
if (!preloadNodes || !preloadNodes.length) {
|
return;
|
}
|
|
var grid = this;
|
var headPreload = this._getHeadPreload();
|
preloadNodes.forEach(function (preloadNode) {
|
var preload = grid._findPreload(preloadNode, headPreload);
|
if (preload) {
|
// Remove the found preload object from the linked list.
|
if (preload.previous) {
|
preload.previous.next = preload.next;
|
}
|
if (preload.next) {
|
preload.next.previous = preload.previous;
|
}
|
}
|
});
|
},
|
|
_getHeadPreload: function () {
|
var headPreload = this.preload;
|
if (headPreload) {
|
while (headPreload.previous) {
|
headPreload = headPreload.previous;
|
}
|
}
|
return headPreload;
|
},
|
|
_findPreload: function (preloadNode, startingPreload) {
|
if (!startingPreload) {
|
startingPreload = this._getHeadPreload();
|
}
|
var preload = startingPreload;
|
while (preload) {
|
if (preload.node === preloadNode) {
|
return preload;
|
}
|
preload = preload.next;
|
}
|
},
|
|
_getRenderedCollection: function (/* preload */) {
|
// This allows extensions to overload the collection retrieval mechanism.
|
return this._renderedCollection;
|
},
|
|
_releaseRange: function (preload, removeBelow, firstRowIndex, lastRowIndex) {
|
if (!preload) {
|
return;
|
}
|
var level = preload.level;
|
var renderedCollection = this._getRenderedCollection(preload);
|
if (lastRowIndex != null) {
|
if (renderedCollection.releaseRange &&
|
typeof firstRowIndex === 'number' && typeof lastRowIndex === 'number') {
|
// Note that currently child rows in Tree structures are never unrendered;
|
// this logic will need to be revisited when that is addressed.
|
|
// releaseRange is end-exclusive, and won't remove anything if start >= end.
|
if (removeBelow) {
|
renderedCollection.releaseRange(lastRowIndex, firstRowIndex + 1);
|
}
|
else {
|
renderedCollection.releaseRange(firstRowIndex, lastRowIndex + 1);
|
}
|
|
if (this._rows && !level) {
|
this._rows[removeBelow ? 'max' : 'min'] = lastRowIndex;
|
if (this._rows.max >= this._total - 1) {
|
this._rows.max = Infinity;
|
}
|
}
|
}
|
}
|
}
|
});
|
});
|