Skip to content

Instantly share code, notes, and snippets.

@nickretallack
Created November 14, 2012 02:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nickretallack/4069815 to your computer and use it in GitHub Desktop.
Save nickretallack/4069815 to your computer and use it in GitHub Desktop.
Virtual Repeat Directive for AngularJS
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