Created
March 29, 2010 16:39
-
-
Save iamjwc/348069 to your computer and use it in GitHub Desktop.
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
<!doctype html> | |
<html> | |
<head> | |
<title></title> | |
<!-- Meta Info --> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
<meta name="description" content="" /> | |
<meta name="keywords" content="" /> | |
<style> | |
#dataTableContainer { | |
position: absolute; | |
left: 0; | |
top: 0; | |
bottom: 0; | |
right: 0; | |
overflow: hidden; | |
} | |
.scroller { | |
position: absolute; | |
top: 17px; | |
bottom: 0; | |
right: 0; | |
width: 16px; | |
overflow-x: hidden; | |
overflow-y: scroll; | |
text-align: right; | |
} | |
.spacer { | |
width: 0px; | |
height: 0px; | |
background-color: transparent; | |
} | |
table { | |
font-size: 10px; | |
-webkit-user-select: none; | |
width: 100%; | |
} | |
thead th { | |
background: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dfdfdf)); | |
border-bottom: 1px solid #525252; | |
border-left: 1px solid #999; | |
font-size: 10px; | |
height: 13px; | |
padding-top: 3px; | |
resize: horizontal; | |
overflow: hidden; | |
white-space: nowrap; | |
} | |
tbody td { | |
border-left: 1px solid #999; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
tbody td:first-child { | |
border-left-width: 0px; | |
} | |
thead th:first-child { | |
border-left-width: 0px; | |
} | |
thead th.gutter { | |
resize: none; | |
width: 17px; | |
} | |
thead th:last-child { | |
resize: none; | |
} | |
thead th.sorted-on { | |
background: -webkit-gradient(linear, left top, left bottom, from(#ccd9ea), to(#a7b7cc)); | |
} | |
thead th.sorted-asc:after { | |
background: -webkit-gradient(linear, left top, left bottom, from(#ccd9ea), to(#a7b7cc)); | |
content: "↑"; | |
} | |
thead th.sorted-desc:after { | |
content: "↓"; | |
} | |
th a { | |
display: block; | |
} | |
tbody td { | |
padding-top: 3px; | |
padding-bottom: 3px; | |
} | |
tr.odd { | |
background-color: #eee; | |
} | |
body.focused tr.selected { | |
background-color: #3372db; | |
color: #fff; | |
} | |
body tr.selected { | |
background-color: #aaa; | |
color: #fff; | |
} | |
th { | |
min-width: 10px; | |
width: 10%; | |
} | |
th, td { | |
text-align: left; | |
padding: 0 3px; | |
} | |
tr th:last-child, tr td:last-child { | |
width: auto; | |
} | |
</style> | |
<!-- Javascripts --> | |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.0/jquery.js"></script> | |
<script type="text/javascript"> | |
Function.prototype.bindFunction = function(thisObject) { | |
var func = this; | |
var args = Array.prototype.slice.call(arguments, 1); | |
return function() { | |
if(func == window) return; | |
return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) | |
}; | |
} | |
Widgets = {} | |
Widgets.Table = function(container, dataSource, apiCalls) { | |
this.buildUIReferences(container); | |
this.handlers(); | |
this.clearSelection(); | |
// | |
this.rowHeight = $('tr', this.ui.tbody).height(); | |
this.api = { | |
rowAt: function(i, tr) { console.log("XXX must support 'rowAt(i, tr)'"); }, | |
headerRow: function(tr) { console.log("XXX must support 'headerRow(tr)'"); }, | |
rowCount: function() { console.log("XXX must support 'rowCount()'"); }, | |
prepareRows: function(i, n) { console.log("XXX can support 'prepareRows(i, n)'"); }, | |
rowClick: function(i) { console.log("XXX can support 'rowClick(i=" + i + ")'"); } | |
}; | |
for(var name in apiCalls) { | |
this.api[name] = apiCalls[name].bindFunction(this); | |
} | |
this.setDataSource(dataSource, true); | |
}; | |
Widgets.Table.prototype = { | |
setDataSource: function(dataSource, invalidateHeader) { | |
this.dataSource = dataSource; | |
this.clearSelection(); | |
this.invalidate(invalidateHeader); | |
}, | |
// Sets up the size of the scroll bar based on the number of rows to be displayed. | |
resizeSpacer: function() { | |
// XXX: If we use this.ui.spacer.height() instead of clientHeight, | |
// scrolling completely breaks. Weird. | |
var currentHeight = this.ui.spacer[0].clientHeight; | |
var computedHeight = this.api.rowCount() * this.rowHeight; | |
if(currentHeight != computedHeight) { | |
this.ui.spacer.height(computedHeight); | |
} | |
}, | |
scroll: function(delta) { | |
this.ui.scroller[0].scrollTop += delta * -10; | |
this.invalidate(); | |
}, | |
scrollRowIntoView: function(i) { | |
this.ui.scroller[0].scrollTop = this.rowHeight * i; | |
}, | |
invalidateRow: function(i) { | |
var row = $('#row_' + i, this.ui.tbody); | |
this.api.rowAt(i, row); | |
this.applySelection(); | |
}, | |
invalidateHeader: function() { | |
var tr = $("<tr></tr>"); | |
this.api.headerRow(tr); | |
this.ui.thead.empty().append(tr); | |
}, | |
invalidate: function(shouldInvalidateHeader) { | |
// Only invalidate the header if forced. | |
if(shouldInvalidateHeader) { | |
this.invalidateHeader(); | |
} | |
this.resizeSpacer(); | |
// HACK: We add one to the limit so that we get one | |
// that hangs out of the viewable area. If we didn't | |
// do this, we would have to resize the window in | |
// increments of rowHeight, or just have a blank line | |
// at the bottom. | |
var offset = this.firstRowDisplayed(); | |
var limit = this.numberOfDisplayedRows() + 1; | |
this.buildRows(offset, limit); | |
this.applySelection(); | |
}, | |
buildRows: function(offset, limit) { | |
this.api.prepareRows(offset, limit); | |
this.ui.tbody.empty(); | |
for(var i = 0; i < limit; ++i) { | |
var id = offset + i; | |
var tr = $("<tr id='row_" + id + "' class='" + ((id % 2) ? "odd" : "even") + "'></tr>"); | |
this.setRowId(tr, id); | |
this.api.rowAt(id, tr); | |
this.ui.tbody.append(tr); | |
} | |
}, | |
setRowId: function(row, i) { | |
row.data('index', i); | |
}, | |
// Number of rows that can fit in the container at a time. | |
numberOfDisplayedRows: function() { | |
var contentHeight = this.ui.scroller[0].clientHeight; | |
var numberOfRowsInRemainingHeight = contentHeight / this.rowHeight; | |
return Math.floor(numberOfRowsInRemainingHeight); | |
}, | |
firstRowDisplayed: function() { | |
var scrollPositionPercentage = Math.abs(this.ui.scroller[0].scrollTop / (this.ui.spacer[0].clientHeight - this.ui.scroller[0].clientHeight)); | |
return Math.floor((this.api.rowCount() - this.numberOfDisplayedRows()) * scrollPositionPercentage); | |
}, | |
rowIsDisplayed: function(i) { | |
var firstRow = this.firstRowDisplayed(); | |
var lastRow = firstRow + this.numberOfDisplayedRows(); | |
return i > this.firstRowDisplayed() && i < lastRow; | |
}, | |
handlers: function() { | |
var self = this; | |
this.ui.container[0].addEventListener('mousewheel', function(e) { | |
var direction = e.wheelDeltaY / Math.abs(e.wheelDeltaY); | |
var numberOfRowsScrolled = Math.floor(Math.abs(e.wheelDeltaY) / self.rowHeight) + 1; | |
var moveBy = numberOfRowsScrolled * self.rowHeight * direction; | |
// XXX: By setting "scrollTop", the scroll handler is also called | |
self.ui.scroller[0].scrollTop -= moveBy; | |
}, false); | |
this.ui.scroller[0].addEventListener('scroll', function(e) { | |
self.invalidate(); | |
}, false); | |
$('tr', this.ui.tbody).live('dblclick', function() { | |
var row = $(this)[0].tagName == 'TR' ? $(this) : $(this).parents('tr'); | |
self.api.rowClick.call(self, row.data('index')); | |
}); | |
$('tr', this.ui.tbody).live('click', function(e) { | |
var id = Number($(this).attr('id').split('_')[1]); | |
if(e.shiftKey && self.selection.length) { | |
self.rangeSelection(self.selection.pop(), id); | |
} else { | |
if(!e.ctrlKey && !e.metaKey) { | |
self.clearSelection(); | |
} | |
self.addToSelection(id); | |
} | |
}); | |
}, | |
clearSelection: function() { | |
this.selection = []; | |
$('tr.selected', this.ui.tbody).removeClass('selected'); | |
}, | |
addToSelection: function(id) { | |
this.selection.push(id); | |
this.applySelection(); | |
}, | |
itemBeforeSelection: function() { | |
var min = _(this.selection).min(); | |
var next = min - 1; | |
if(next < 0) { | |
throw("cannot grow in this direction"); | |
} else { | |
return next; | |
} | |
}, | |
itemAfterSelection: function() { | |
var max = _(this.selection).max(); | |
var next = max + 1; | |
if(next >= this.api.rowCount()) { | |
throw("cannot grow in this direction"); | |
} else { | |
return next; | |
} | |
}, | |
selectItemUp: function() { | |
var item = this.itemBeforeSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.clearSelection(); | |
this.addToSelection(item); | |
}, | |
selectItemDown: function() { | |
var item = this.itemAfterSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.clearSelection(); | |
this.addToSelection(item); | |
}, | |
growSelectionUp: function() { | |
var item = this.itemBeforeSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.addToSelection(item); | |
}, | |
growSelectionDown: function() { | |
var item = this.itemAfterSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.addToSelection(item); | |
}, | |
rangeSelection: function(start, end) { | |
if(start > end) { | |
var tmp = start; | |
start = end; | |
end = tmp; | |
} | |
for(var i = start; i <= end; ++i) { | |
this.addToSelection(i); | |
} | |
}, | |
applySelection: function() { | |
for(var i = 0; i < this.selection.length; ++i) { | |
var id = this.selection[i]; | |
$('tr#row_' + id, this.ui.tbody).addClass('selected'); | |
} | |
}, | |
buildUIReferences: function(container) { | |
this.ui = {}; | |
this.ui.container = container; | |
this.ui.table = $('> table', this.ui.container); | |
this.ui.thead = $('> table > thead', this.ui.container); | |
this.ui.tbody = $('> table > tbody', this.ui.container); | |
this.ui.scroller = $('> .scroller', this.ui.container); | |
this.ui.spacer = $('> .scroller .spacer', this.ui.container); | |
} | |
}; | |
$(function() { | |
new Widgets.Table($('#dataTableContainer'), null, { | |
headerRow: function(tr) { | |
tr.html("<th>Header Column 1</th><th>Header Column 2</th>"); | |
}, | |
rowAt: function(i, tr) { | |
tr.html("<td>Column 1, Row " + i + "</td><td>Column 2, Row " + i + "</td>"); | |
}, | |
rowCount: function() { | |
return 1000000; | |
} | |
}); | |
}); | |
</script> | |
</head> | |
<body class="focused"> | |
<div id="dataTableContainer"> | |
<table cellpadding="0" cellspacing="0"> | |
<thead></thead> | |
<tbody> | |
<tr class="row"> | |
<td>Loading</td> | |
</tr> | |
</tbody> | |
</table> | |
<div class="scroller"> | |
<div class="spacer"></div> | |
</div> | |
</div> | |
</body> | |
</html> |
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
<!doctype html> | |
<html> | |
<head> | |
<title></title> | |
<!-- Meta Info --> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
<meta name="description" content="" /> | |
<meta name="keywords" content="" /> | |
<style> | |
#dataTableContainer { | |
position: absolute; | |
left: 0; | |
top: 0; | |
bottom: 0; | |
right: 0; | |
overflow: hidden; | |
} | |
.scroller { | |
position: absolute; | |
top: 30px; | |
bottom: 0; | |
right: 0; | |
width: 16px; | |
overflow-x: hidden; | |
overflow-y: scroll; | |
text-align: right; | |
} | |
.spacer { | |
width: 0px; | |
height: 0px; | |
background-color: transparent; | |
} | |
</style> | |
<!-- Javascripts --> | |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.0/jquery.js"></script> | |
<script type="text/javascript"> | |
Function.prototype.bindFunction = function(thisObject) { | |
var func = this; | |
var args = Array.prototype.slice.call(arguments, 1); | |
return function() { | |
if(func == window) return; | |
return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) | |
}; | |
} | |
Widgets = {} | |
Widgets.Table = function(container, dataSource, apiCalls) { | |
this.buildUIReferences(container); | |
this.handlers(); | |
this.clearSelection(); | |
// | |
this.rowHeight = $('tr', this.ui.tbody).height(); | |
this.api = { | |
rowAt: function(i, tr) { console.log("XXX must support 'rowAt(i, tr)'"); }, | |
headerRow: function(tr) { console.log("XXX must support 'headerRow(tr)'"); }, | |
rowCount: function() { console.log("XXX must support 'rowCount()'"); }, | |
prepareRows: function(i, n) { console.log("XXX can support 'prepareRows(i, n)'"); }, | |
rowClick: function(i) { console.log("XXX can support 'rowClick(i=" + i + ")'"); } | |
}; | |
for(var name in apiCalls) { | |
this.api[name] = apiCalls[name].bindFunction(this); | |
} | |
this.setDataSource(dataSource, true); | |
}; | |
Widgets.Table.prototype = { | |
setDataSource: function(dataSource, invalidateHeader) { | |
this.dataSource = dataSource; | |
this.clearSelection(); | |
this.invalidate(invalidateHeader); | |
}, | |
// Sets up the size of the scroll bar based on the number of rows to be displayed. | |
resizeSpacer: function() { | |
// XXX: If we use this.ui.spacer.height() instead of clientHeight, | |
// scrolling completely breaks. Weird. | |
var currentHeight = this.ui.spacer[0].clientHeight; | |
var computedHeight = this.api.rowCount() * this.rowHeight; | |
if(currentHeight != computedHeight) { | |
this.ui.spacer.height(computedHeight); | |
} | |
}, | |
scroll: function(delta) { | |
this.ui.scroller[0].scrollTop += delta * -10; | |
this.invalidate(); | |
}, | |
scrollRowIntoView: function(i) { | |
this.ui.scroller[0].scrollTop = this.rowHeight * i; | |
}, | |
invalidateRow: function(i) { | |
var row = $('#row_' + i, this.ui.tbody); | |
this.api.rowAt(i, row); | |
this.applySelection(); | |
}, | |
invalidateHeader: function() { | |
var tr = $("<tr></tr>"); | |
this.api.headerRow(tr); | |
this.ui.thead.empty().append(tr); | |
}, | |
invalidate: function(shouldInvalidateHeader) { | |
// Only invalidate the header if forced. | |
if(shouldInvalidateHeader) { | |
this.invalidateHeader(); | |
} | |
this.resizeSpacer(); | |
// HACK: We add one to the limit so that we get one | |
// that hangs out of the viewable area. If we didn't | |
// do this, we would have to resize the window in | |
// increments of rowHeight, or just have a blank line | |
// at the bottom. | |
var offset = this.firstRowDisplayed(); | |
var limit = this.numberOfDisplayedRows() + 1; | |
this.buildRows(offset, limit); | |
this.applySelection(); | |
}, | |
buildRows: function(offset, limit) { | |
this.api.prepareRows(offset, limit); | |
this.ui.tbody.empty(); | |
for(var i = 0; i < limit; ++i) { | |
var id = offset + i; | |
var tr = $("<tr id='row_" + id + "' class='" + ((id % 2) ? "odd" : "even") + "'></tr>"); | |
this.setRowId(tr, id); | |
this.api.rowAt(id, tr); | |
this.ui.tbody.append(tr); | |
} | |
}, | |
setRowId: function(row, i) { | |
row.data('index', i); | |
}, | |
// Number of rows that can fit in the container at a time. | |
numberOfDisplayedRows: function() { | |
var contentHeight = this.ui.scroller[0].clientHeight; | |
var numberOfRowsInRemainingHeight = contentHeight / this.rowHeight; | |
return Math.floor(numberOfRowsInRemainingHeight); | |
}, | |
firstRowDisplayed: function() { | |
var scrollPositionPercentage = Math.abs(this.ui.scroller[0].scrollTop / (this.ui.spacer[0].clientHeight - this.ui.scroller[0].clientHeight)); | |
return Math.floor((this.api.rowCount() - this.numberOfDisplayedRows()) * scrollPositionPercentage); | |
}, | |
rowIsDisplayed: function(i) { | |
var firstRow = this.firstRowDisplayed(); | |
var lastRow = firstRow + this.numberOfDisplayedRows(); | |
return i > this.firstRowDisplayed() && i < lastRow; | |
}, | |
handlers: function() { | |
var self = this; | |
this.ui.container[0].addEventListener('mousewheel', function(e) { | |
var direction = e.wheelDeltaY / Math.abs(e.wheelDeltaY); | |
var numberOfRowsScrolled = Math.floor(Math.abs(e.wheelDeltaY) / self.rowHeight) + 1; | |
var moveBy = numberOfRowsScrolled * self.rowHeight * direction; | |
// XXX: By setting "scrollTop", the scroll handler is also called | |
self.ui.scroller[0].scrollTop -= moveBy; | |
}, false); | |
this.ui.scroller[0].addEventListener('scroll', function(e) { | |
self.invalidate(); | |
}, false); | |
$('tr', this.ui.tbody).live('dblclick', function() { | |
var row = $(this)[0].tagName == 'TR' ? $(this) : $(this).parents('tr'); | |
self.api.rowClick.call(self, row.data('index')); | |
}); | |
$('tr', this.ui.tbody).live('click', function(e) { | |
var id = Number($(this).attr('id').split('_')[1]); | |
if(e.shiftKey && self.selection.length) { | |
self.rangeSelection(self.selection.pop(), id); | |
} else { | |
if(!e.ctrlKey && !e.metaKey) { | |
self.clearSelection(); | |
} | |
self.addToSelection(id); | |
} | |
}); | |
}, | |
clearSelection: function() { | |
this.selection = []; | |
$('tr.selected', this.ui.tbody).removeClass('selected'); | |
}, | |
addToSelection: function(id) { | |
this.selection.push(id); | |
this.applySelection(); | |
}, | |
itemBeforeSelection: function() { | |
var min = _(this.selection).min(); | |
var next = min - 1; | |
if(next < 0) { | |
throw("cannot grow in this direction"); | |
} else { | |
return next; | |
} | |
}, | |
itemAfterSelection: function() { | |
var max = _(this.selection).max(); | |
var next = max + 1; | |
if(next >= this.api.rowCount()) { | |
throw("cannot grow in this direction"); | |
} else { | |
return next; | |
} | |
}, | |
selectItemUp: function() { | |
var item = this.itemBeforeSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.clearSelection(); | |
this.addToSelection(item); | |
}, | |
selectItemDown: function() { | |
var item = this.itemAfterSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.clearSelection(); | |
this.addToSelection(item); | |
}, | |
growSelectionUp: function() { | |
var item = this.itemBeforeSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.addToSelection(item); | |
}, | |
growSelectionDown: function() { | |
var item = this.itemAfterSelection(); | |
if(!this.rowIsDisplayed(item)) { | |
this.scrollRowIntoView(item); | |
} | |
this.addToSelection(item); | |
}, | |
rangeSelection: function(start, end) { | |
if(start > end) { | |
var tmp = start; | |
start = end; | |
end = tmp; | |
} | |
for(var i = start; i <= end; ++i) { | |
this.addToSelection(i); | |
} | |
}, | |
applySelection: function() { | |
for(var i = 0; i < this.selection.length; ++i) { | |
var id = this.selection[i]; | |
$('tr#row_' + id, this.ui.tbody).addClass('selected'); | |
} | |
}, | |
buildUIReferences: function(container) { | |
this.ui = {}; | |
this.ui.container = container; | |
this.ui.table = $('> table', this.ui.container); | |
this.ui.thead = $('> table > thead', this.ui.container); | |
this.ui.tbody = $('> table > tbody', this.ui.container); | |
this.ui.scroller = $('> .scroller', this.ui.container); | |
this.ui.spacer = $('> .scroller .spacer', this.ui.container); | |
} | |
}; | |
$(function() { | |
new Widgets.Table($('#dataTableContainer'), null, { | |
headerRow: function(tr) { | |
tr.html("<th>Header Column 1</th><th>Header Column 2</th>"); | |
}, | |
rowAt: function(i, tr) { | |
tr.html("<td>Column 1, Row " + i + "</td><td>Column 2, Row " + i + "</td>"); | |
}, | |
rowCount: function() { | |
return 1000000; | |
} | |
}); | |
}); | |
</script> | |
</head> | |
<body class="focused"> | |
<div id="dataTableContainer"> | |
<table border="1"> | |
<thead></thead> | |
<tbody> | |
<tr class="row"> | |
<td>Loading</td> | |
</tr> | |
</tbody> | |
</table> | |
<div class="scroller"> | |
<div class="spacer"></div> | |
</div> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment