Created
November 14, 2012 02:04
-
-
Save nickretallack/4069815 to your computer and use it in GitHub Desktop.
Virtual Repeat Directive for AngularJS
This file contains 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
module = angular.module 'ui.directive' | |
module.directive 'virtualRepeat', -> | |
transclude: true | |
compile: (element, attributes, linker) -> | |
(scope, element, attributes, controller) -> | |
expression = attributes.virtualRepeat | |
match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/) | |
if not match then throw Error "Expected virtualRepeat in form of '_item_ in _collection_' but got '#{expression}'." | |
[throwaway, key, value] = match | |
# Determine cell sizing | |
cell_size = height: parseInt attributes.cellHeight | |
width = attributes.cellWidth | |
if width in ['*',undefined] | |
grid_mode = false | |
cell_size.width = 0 | |
else | |
grid_mode = true | |
cell_size.width = parseInt width | |
padding = parseInt attributes.padding | |
data = scope.$eval value | |
grid = new SlickGriddle element, data, scope, linker, | |
grid_mode: grid_mode | |
cell_size: cell_size | |
key: key | |
padding: padding | |
if grid_mode | |
$(window).resize -> scope.$apply -> grid.resize() | |
scope.$watch value, (data) -> | |
grid.data = data | |
data_should_change() | |
data.onDataLoaded.subscribe -> | |
grid.invalidate() | |
# TODO: maybe this stuff should go in data instead? | |
data_should_change = -> | |
### Parameters have changed, so the data should change now. | |
We probably want to view the new data set from the top. ### | |
$(window).scrollTop 0 | |
data.refresh() | |
grid.notifyData() | |
scope.$watch 'search', (value) -> | |
data.setFilterArgs search:value | |
data_should_change() | |
scope.$watch 'version', (value, old, scope) -> | |
data.setVersion value | |
data_should_change() | |
scope.$on 'sort', (event, column, ascending) -> | |
attribute = available_info_columns[column].field | |
data.setSort | |
sort_column:attribute | |
sort_ascending:ascending | |
data_should_change() | |
scope.$on 'files-changed', -> | |
data_should_change() | |
scope.$on 'reload', (data) -> | |
data_should_change() | |
delay = (a,b) -> setTimeout b,a | |
async = (a) -> setTimeout a,0 | |
class SlickGriddle | |
constructor:(@canvas, @data, @scope, @linker, {@cell_size, @post_render_cell, @viewport, @grid_mode, @render_loading, @key, @padding}) -> | |
@canvas = $(@canvas) | |
@frame_rate ?= 5 | |
@padding ?= 0 | |
if (@canvas.css 'position') not in ['relative','absolute','fixed'] | |
@canvas.css position:'relative' | |
@viewport ?= $ window | |
@canvas_node = @canvas[0] | |
@node_cache = {} | |
@postProcessedItems = {} | |
# start drawing | |
@resize() | |
@viewport.scroll => async => @scope.$apply => @render() | |
resize: -> @invalidate() | |
calculate_stuff: -> | |
""" Makes decisions based on the length of the data and the size of the window. | |
These variables must be recalculated if either changes. """ | |
@data_length = @getDataLength() | |
@viewport_height = @viewport.height() | |
# Figure out how far the top of the canvas is down from the top of the document it's in. | |
@offset = 0 | |
element = @canvas_node | |
while element isnt @viewport[0] and element isnt null | |
@offset += element.offsetTop | |
element = element.offsetParent | |
canvas_width = @canvas.width() + @padding | |
@items_per_row = if @grid_mode then Math.floor canvas_width / @cell_size.width else 1 | |
@total_rows = Math.ceil @data_length / @items_per_row | |
@screenful_of_rows = Math.ceil @viewport_height / @cell_size.height | |
@canvas_height = @cell_size.height * @total_rows | |
@canvas.css height: "#{@canvas_height}px" | |
getDataLength: -> if @data.getLength? then @data.getLength() else @data.length | |
getDataItem: (index) -> if @data.getItem? then @data.getItem(index) else @data[index] | |
cleanupNodes: (rangeToKeep) -> | |
for item_index of @node_cache | |
unless rangeToKeep.top <= Math.floor(item_index / @items_per_row) <= rangeToKeep.bottom | |
@uncacheNode item_index | |
invalidate: -> | |
@postProcessedItems = {} | |
for item_index of @node_cache | |
@uncacheNode item_index | |
@calculate_stuff() | |
@old_render_range = null | |
@render() | |
uncacheNode: (index) -> | |
if index of @node_cache | |
@node_cache[index].node.remove() | |
delete @node_cache[index] | |
cancel = @postProcessedItems[index] | |
cancel?() | |
delete @postProcessedItems[index] | |
postProcess: -> | |
range = @getVisibleRange() | |
for row_index in [range.top..range.bottom] | |
for column_index in [0...@items_per_row] | |
item_index = row_index * @items_per_row + column_index | |
continue if @postProcessedItems[item_index] | |
node = @node_cache[item_index] | |
continue unless node | |
item = @getDataItem item_index | |
cancel = @post_render_cell node, item_index, item | |
@postProcessedItems[item_index] = cancel | |
notifyData: -> | |
@data.ensureData? @getViewport() | |
render: -> | |
render_range = @getRenderRange() | |
@notifyData() unless @old_render_range? and _.isEqual render_range, @old_render_range | |
@old_render_range = render_range | |
@cleanupNodes render_range | |
for row_index in [render_range.top..render_range.bottom] | |
for column_index in [0...@items_per_row] | |
item_index = row_index * @items_per_row + column_index | |
continue if @node_cache[item_index]? | |
break if item_index >= @data_length | |
item = @getDataItem item_index | |
continue unless item | |
position = | |
top:row_index * @cell_size.height | |
left:column_index * @cell_size.width | |
index:item_index | |
odd_even: if item_index % 2 is 0 then 'even' else 'odd' | |
do (item, item_index, position) => | |
scope = @scope.$new() | |
scope['$position'] = position | |
scope[@key] = item | |
@linker scope, (node) => | |
node.css | |
position: 'absolute' | |
top: "#{position.top}px" | |
left: "#{position.left}px" | |
@node_cache[item_index] = | |
node: node | |
scope: scope | |
@canvas.append node | |
if @post_render_cell | |
delay 500, => @postProcess() | |
getVisibleRange: -> | |
""" Returns the top and bottom rows the user can see """ | |
scrollTop = $(@viewport).scrollTop() - @offset | |
top: Math.max 0, Math.floor scrollTop / @cell_size.height | |
bottom: Math.max 0, Math.floor (scrollTop + @viewport_height) / @cell_size.height | |
getRenderRange: -> | |
""" Pads the visible range with a screenful of buffer rows in both directions, | |
in case the user scrolls quickly. """ | |
buffer = @screenful_of_rows | |
range = @getVisibleRange() | |
top: Math.max 0, range.top - buffer | |
bottom: Math.min @total_rows, range.bottom + buffer | |
getViewport: -> | |
""" Returns the range of data items that will be rendered. | |
It's the same as getRenderRange unless you have more than one item per row """ | |
visible = @getRenderRange() | |
top:visible.top * @items_per_row | |
bottom:visible.bottom * @items_per_row |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment