Created
May 19, 2011 18:04
-
-
Save willbailey/981352 to your computer and use it in GitHub Desktop.
BoltJS TableView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ### TableView | |
// TableView provides an efficient mechanism for progressively | |
// rendering from a data source provided by the owner object. | |
// Cells are queued for reuse when they go offscreen and then | |
// translated back into position with updated content as they | |
// are reused. | |
var TableView = exports.TableView = core.createClass({ | |
name: 'TableView', | |
extend: View, | |
mixins: [HasEventListeners], | |
declare: function(options) { | |
return { | |
eventListeners: { | |
'touchstart,touchend,touchcancel,touchmove .bt-table-view-cell': 'onCellTouch' | |
}, | |
debug: false, | |
style: {height: '100%'}, | |
flex: 1, | |
boxOrientation: 'vertical', | |
bufferSize: 0, | |
loading: false, | |
sectioned: true, | |
childViews: [{ | |
view: 'ScrollView', | |
owner: this, | |
ref: 'scrollView', | |
flex: 1 | |
}] | |
}; | |
}, | |
// after the setup method is invoked we cache a few references for | |
// later use. | |
ready: function() { | |
this.owner = this.getOwner(); | |
this.scrollView = this.findRef('scrollView'); | |
this.bufferSize = this.getBufferSize(); | |
this.y = 0; | |
this.lastY = 0; | |
this.lastX = 0; | |
if (this.getFixedSectionHeaderHeight) { | |
this.fixedSectionHeaderHeight = this.getFixedSectionHeaderHeight(); | |
} | |
if (this.getFixedRowHeight) { | |
this.fixedRowHeight = this.getFixedRowHeight(); | |
} | |
}, | |
destroy: function() { | |
this.superKlass.destroy.call(this); | |
}, | |
// clear the currently selected cell | |
clearSelection: function() { | |
this.selectedCell && this.selectedCell.setSelected(false); | |
}, | |
onDocumentInsertion: function() { | |
this.refresh(); | |
}, | |
// Fired when the user taps on a cell | |
onCellTouch: function(e, elem) { | |
var idx = elem.getAttribute('data-item-index'); | |
this.log(e.type); | |
var item = this.itemList[idx]; | |
var view = item && item.view; | |
var owner = this.getOwner(); | |
if (!view) return; | |
switch(e.type) { | |
case 'touchstart': | |
this._cellSelectTimer = setTimeout(util.bind(function() { | |
this.clearSelection(); | |
view.setSelected(true); | |
this.selectedCell = view; | |
}, this), 50); | |
break; | |
case 'touchend': | |
if (view.getSelected()) { | |
owner.cellSelectedAtRowInSection(this, item.row, item.section, view); | |
} | |
break; | |
case 'touchmove': | |
clearTimeout(this._cellSelectTimer); | |
this.clearSelection(); | |
break; | |
case 'touchcancel': | |
clearTimeout(this._cellSelectTimer); | |
this.clearSelection(); | |
break; | |
default: | |
break; | |
} | |
}, | |
// Call refresh whenever you want to do a full refresh of the table. | |
// The table's size will be recalculated and it will be freshly buffered | |
// to the current scroll coordinates. | |
refresh: function() { | |
delete this.viewPort; | |
delete this.scrollHeight; | |
delete this.sectionCount; | |
delete this.rowCounts; | |
delete this.itemList; | |
delete this.lastHeadPtr; | |
delete this.lastTailPtr; | |
this.clear(); | |
this.buffer(); | |
}, | |
// Delegate callback from the scrollview indicating that it scrolled to the | |
// current x, y coordinates. | |
scrollViewDidScrollTo: function(scrollView, x, y) { | |
this.x = -x; | |
this.y = -y; | |
if (this.y !== this.lastY) { | |
this.buffer(); | |
this.lastY = y; | |
this.lastX = x; | |
} | |
}, | |
// clear all childviews; | |
clear: function() { | |
this.clearSelection && this.clearSelection(); | |
util.forEach(this.scrollView.getChildViews() || [], function(view) { | |
view.destroy(); | |
}); | |
}, | |
// buffer the data by rendering the delta between the old and new scroll | |
// positions and cleaninup up anything unused. | |
buffer: function() { | |
this.calclulateBuffer(this.y); | |
if (this.lastHeadPtr !== this.headPtr || this.lastTailPtr !== this.tailPtr) { | |
this.renderBuffer( | |
this.headPtr, | |
this.tailPtr, | |
this.lastHeadPtr, | |
this.lastTailPtr); | |
} | |
}, | |
// Determine the desired buffer for the given y coordinate. This buffer will | |
// be used to determine which unused cells can be cleaned up and which cells | |
// to use when populating the empty region after the scroll. | |
calclulateBuffer: function(y) { | |
if (!this.viewPort) this.calculateViewPort(); | |
if (!this.scrollHeight) this.calculateScrollHeight(); | |
this.viewPortHeadPtr = this.findItemIndex(Math.max(y, 0)); | |
this.viewPortTailPtr = this.findItemIndex( | |
Math.min((y + this.viewPort.height), this.scrollHeight)); | |
// The head and tail pointers provide an extra buffer around the view port | |
// allowing for some latency between rendering and scrolling. Increasing | |
// the buffer can be configured from the owner class and may help reduce | |
// flickering in some cases. | |
this.headPtr = Math.max(this.viewPortHeadPtr - this.bufferSize, 0); | |
this.tailPtr = Math.min( | |
this.viewPortTailPtr + this.bufferSize, | |
this.itemList.length - 1); | |
}, | |
// Render the current buffer window by calculating the direction of | |
// movement, rendering items to fill the revealed portion of the buffer | |
// and reaping items from the portion of the prior buffer not included | |
// in the current buffer. | |
// | |
// For Example in a downward scroll: | |
// | |
// ------------- | |
// <- last head ptr | |
// BUFFER <- to be reaped | |
// <- current head ptr | |
// ------------- | |
// | |
// VIEW PORT | |
// | |
// ------------- | |
// <- last tail ptr | |
// BUFFER <- to be rendered | |
// <- current tail ptr | |
// ------------- | |
renderBuffer: function(headPtr, tailPtr, lastHeadPtr, lastTailPtr) { | |
var direction; | |
if (lastHeadPtr === undefined && lastTailPtr === undefined) { | |
direction = 'refresh'; | |
} else if (tailPtr > lastTailPtr || headPtr > lastHeadPtr) { | |
direction = 'down'; | |
} else if (tailPtr < lastTailPtr || headPtr < lastHeadPtr) { | |
direction = 'up'; | |
} | |
var i; | |
switch (direction) { | |
// If we don't have a prior state, then just render the entire viewport. | |
case 'refresh': | |
for (i = headPtr; i <= tailPtr; i++) { | |
this.renderItem(this.itemList[i], i); | |
} | |
break; | |
// The user is scrolling down the table so reap from the head and render | |
// to the tail. | |
case 'down': | |
for (i = lastHeadPtr; i < headPtr; i++) { | |
this.reapItem(this.itemList[i]); | |
} | |
for (i = lastTailPtr + 1; i <= tailPtr; i++) { | |
this.renderItem(this.itemList[i], i); | |
} | |
break; | |
// the user is scrolling up the table so reap from the tail and render | |
// to the head. | |
case 'up': | |
for (i = lastTailPtr; i > tailPtr; i--) { | |
this.reapItem(this.itemList[i]); | |
} | |
for (i = lastHeadPtr - 1; i >= headPtr; i--) { | |
this.renderItem(this.itemList[i], i); | |
} | |
break; | |
default: | |
break; | |
} | |
this.lastHeadPtr = this.headPtr; | |
this.lastTailPtr = this.tailPtr; | |
}, | |
// To render an item we ask our owner to provide the cell or section header. | |
// We then apply the appropriate transform so the item appears at the | |
// appropriate index. | |
renderItem: function(item, index) { | |
if (item.view) return; | |
if (item.hasOwnProperty('row')) { | |
view = this.owner.cellForRowInSection(this, item.row, item.section); | |
view.setMetadata({'item-index': index}); | |
} else { | |
view = this.owner.viewForHeaderInSection(this, item.section); | |
view.setMetadata({'item-index': index}); | |
} | |
view.setStyle({webkitTransform: 'translate3d(0,' + item.start + 'px,0)'}); | |
item.view = view; | |
if (!view.getNode().parentNode) { | |
this.scrollView.appendChild(view); | |
} | |
}, | |
// Items that are no longer within the buffer range can be reaped and queued | |
// for reuse. When an item is reaped we delete it from the item in the item | |
// cache to signal that it can no longer be rendered without refetching from | |
// the data source. We then push the item onto the appropriate section or | |
// cell queue and hide the dom representation offscreen. | |
reapItem: function(item) { | |
var view = item.view; | |
if (!view) return; | |
delete item.view; | |
if (item.hasOwnProperty('row')) { | |
this.enqueueReusableCellWithIdentifier( | |
view, | |
view.getReuseIdentifier()); | |
} else { | |
this.enqueueReusableSectionHeaderWithIdentifier( | |
view, | |
view.getReuseIdentifier()); | |
} | |
view.setStyle({webkitTransform: 'translate3d(-5000px,0,0)'}); | |
view.setMetadata({'item-index': 'queued'}); | |
}, | |
// Use binary search to find the item in the itemList that wraps the | |
// requested coordinate. | |
findItemIndex: function(y) { | |
var high = itemList.length; low = 0; | |
while(low < high) { | |
mid = (low + high) >> 1; | |
var item = itemList[mid]; | |
if (item.start <= y && item.end >= y) { | |
return mid; | |
} else if (y < item.end) { | |
high = mid; | |
} else { | |
low = mid + 1; | |
} | |
} | |
return null; | |
}, | |
// Calculate the total scroll height of the view by iterating the data | |
// source and marking where each view begins and ends. This is cached until | |
// the invalidate method is called on the view. | |
calculateScrollHeight: function() { | |
window.itemList = this.itemList = []; | |
this.scrollHeight = 0; | |
if (this.getSectioned()) { | |
this.sectionCount = this.hasOwnProperty('sectionCount') ? this.sectionCount : this.owner.numberOfSections(); | |
} else { | |
this.sectionCount = 1; | |
} | |
this.rowCounts = this.rowCounts || {}; | |
var absoluteIndex = 0; | |
var item; | |
for (var i = 0; i < this.sectionCount; i++) { | |
var sectionHeight; | |
if (this.hasOwnProperty('fixedSectionHeaderHeight')) { | |
sectionHeight += this.fixedSectionHeaderHeight; | |
} else { | |
sectionHeight += this.owner.heightForSectionHeader && this.owner.heightForSectionHeader(i); | |
} | |
if (sectionHeight > 0) { | |
this.scrollHeight += sectionHeight; | |
item = {section: i, start: this.scrollHeight, end: this.scrollHeight}; | |
this.itemList.push(item); | |
} | |
if (this.rowCounts[i] === undefined) { | |
this.rowCounts[i] = this.owner.numberOfRowsInSection(i); | |
} | |
for (var j = 0; j < this.rowCounts[i]; j++) { | |
item = {section: i, row: j, start: this.scrollHeight}; | |
if (this.hasOwnProperty('fixedRowHeight')) { | |
this.scrollHeight += this.fixedRowHeight; | |
} else { | |
this.scrollHeight += this.owner.heightForRowInSection(j, i); | |
} | |
item.end = this.scrollHeight; | |
itemList.push(item); | |
} | |
this.absoluteIndex++; | |
} | |
this.scrollView.setContentHeight(this.scrollHeight); | |
this.scrollView.refresh(); | |
}, | |
// calculate the view port by grabbing the rectangle from the scroll view. | |
calculateViewPort: function() { | |
return this.viewPort = this.scrollView.getRect(); | |
}, | |
// pull a cell from the queue and return it. The cell's contents can | |
// then be modified for reuse. | |
dequeueReusableCellWithIdentifier: function(identifier) { | |
this.cells = this.cells || {}; | |
this.cells[identifier] = this.cells[identifier] || []; | |
return this.cells[identifier].shift(); | |
}, | |
// pull a section header from the queue and return it. The section header's | |
// contents can then be modified for reuse. | |
dequeueReusableSectionHeaderWithIdentifier: function(identifier) { | |
this.sectionHeaders = this.sectionHeaders || {}; | |
this.sectionHeaders[identifier] = this.sectionHeaders[identifier] || []; | |
return this.sectionHeaders[identifier].shift(); | |
}, | |
// enqueue a cell for reuse | |
enqueueReusableCellWithIdentifier: function(cell, identifier) { | |
this.cells = this.cells || {}; | |
this.cells[identifier] = this.cells[identifier] || []; | |
return this.cells[identifier].push(cell); | |
}, | |
// enqueue a section header for reuse | |
enqueueReusableSectionHeaderWithIdentifier: function(header, identifier) { | |
this.sections = this.sections || {}; | |
this.sections[identifier] = this.sections[identifier] || []; | |
return this.sections[identifier].push(header); | |
}, | |
// configurable log for debugging. | |
log: function(message) { | |
if (this.getDebug()) console.log.apply(console, arguments); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment