Skip to content

Instantly share code, notes, and snippets.

@iamjwc
Created January 27, 2010 18:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iamjwc/288082 to your computer and use it in GitHub Desktop.
Save iamjwc/288082 to your computer and use it in GitHub Desktop.
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment