'use strict'
_ = require 'underscore'
Backbone = require 'backbone'
View = require 'chaplin/views/view'
utils = require 'chaplin/lib/utils''use strict'
_ = require 'underscore'
Backbone = require 'backbone'
View = require 'chaplin/views/view'
utils = require 'chaplin/lib/utils'Shortcut to access the DOM manipulation library.
$ = Backbone.$
filterChildren = (nodeList, selector) ->
return nodeList unless selector
for node in nodeList when Backbone.utils.matchesSelector node, selector
node
toggleElement = do ->
if $
(elem, visible) -> elem.toggle visible
else
(elem, visible) ->
elem.style.display = (if visible then '' else 'none')
addClass = do ->
if $
(elem, cls) -> elem.addClass cls
else
(elem, cls) -> elem.classList.add cls
startAnimation = do ->
if $
(elem, useCssAnimation, cls) ->
if useCssAnimation
addClass elem, cls
else
elem.css 'opacity', 0
else
(elem, useCssAnimation, cls) ->
if useCssAnimation
addClass elem, cls
else
elem.style.opacity = 0
endAnimation = do ->
if $
(elem, duration) -> elem.animate {opacity: 1}, duration
else
(elem, duration) ->
elem.style.transition = "opacity #{(duration / 1000)}s"
elem.opacity = 1
insertView = do ->
if $
(list, viewEl, position, length, itemSelector) ->
insertInMiddle = (0 < position < length)
isEnd = (length) -> length is 0 or position is length
if insertInMiddle or itemSelectorGet the children which originate from item views.
children = list.children itemSelector
childrenLength = children.lengthCheck if it needs to be inserted.
unless children[position] is viewEl
if isEnd childrenLengthInsert at the end.
list.append viewEl
elseInsert at the right position.
if position is 0
children.eq(position).before viewEl
else
children.eq(position - 1).after viewEl
else
method = if isEnd length then 'append' else 'prepend'
list[method] viewEl
else
(list, viewEl, position, length, itemSelector) ->
insertInMiddle = (0 < position < length)
isEnd = (length) -> length is 0 or position is length
if insertInMiddle or itemSelectorGet the children which originate from item views.
children = filterChildren list.children, itemSelector
childrenLength = children.lengthCheck if it needs to be inserted.
unless children[position] is viewEl
if isEnd childrenLengthInsert at the end.
list.appendChild viewEl
else if position is 0Insert at the right position.
list.insertBefore viewEl, children[position]
else
last = children[position - 1]
if list.lastChild is last
list.appendChild viewEl
else
list.insertBefore viewEl, last.nextElementSibling
else if isEnd length
list.appendChild viewEl
else
list.insertBefore viewEl, list.firstChildGeneral class for rendering Collections.
Derive this class and declare at least itemView or override
initItemView. initItemView gets an item model and should instantiate
and return a corresponding item view.
module.exports = class CollectionView extends ViewThese options may be overwritten in derived classes.
A class of item in collection. This property has to be overridden by a derived class.
itemView: nullPer default, render the view itself and all items on creation.
autoRender: true
renderItems: trueWhen new items are added, their views are faded in. Animation duration in milliseconds (set to 0 to disable fade in)
animationDuration: 500By default, fading in is done by javascript function which can be slow on mobile devices. CSS animations are faster, but require user’s manual definitions.
useCssAnimation: falseCSS classes that will be used when hiding / showing child views.
animationStartClass: 'animated-item-view'
animationEndClass: 'animated-item-view-end'A collection view may have a template and use one of its child elements
as the container of the item views. If you specify listSelector, the
item views will be appended to this element. If empty, $el is used.
listSelector: nullThe actual element which is fetched using listSelector
$list: nullSelector for a fallback element which is shown if the collection is empty.
fallbackSelector: nullThe actual element which is fetched using fallbackSelector
$fallback: nullSelector for a loading indicator element which is shown while the collection is syncing.
loadingSelector: nullThe actual element which is fetched using loadingSelector
$loading: nullSelector which identifies child elements belonging to collection If empty, all children of $list are considered. Not null, because of Zepto bug https://github.com/madrobby/zepto/pull/768.
itemSelector: undefinedThe filter function, if any.
filterer: nullA function that will be executed after each filter. Hides excluded items by default.
filterCallback: (view, included) ->
view.$el.stop(true, true) if $
toggleElement (if $ then view.$el else view.el), includedTrack a list of the visible views.
visibleItems: null optionNames: View::optionNames.concat ['renderItems', 'itemView']
constructor: (options) ->Initialize list for visible items.
@visibleItems = []
super initialize: (options = {}) -> @addCollectionListeners()Apply a filter if one provided.
@filter options.filterer if options.filterer?Binding of collection listeners.
addCollectionListeners: ->
@listenTo @collection, 'add', @itemAdded
@listenTo @collection, 'remove', @itemRemoved
@listenTo @collection, 'reset sort', @itemsResetOverride View#getTemplateData, don’t serialize collection items here.
getTemplateData: ->
templateData = {length: @collection.length}If the collection is a SyncMachine, add a synced flag.
if typeof @collection.isSynced is 'function'
templateData.synced = @collection.isSynced()
templateDataIn contrast to normal views, a template is not mandatory
for CollectionViews. Provide an empty getTemplateFunction.
getTemplateFunction: ->Main render method (should be called only once)
render: ->
superSet the $list property with the actual list container.
listSelector = _.result this, 'listSelector'
if $
@$list = if listSelector then @$(listSelector) else @$el
else
@list = if listSelector then @find(@listSelector) else @el
@initFallback()
@initLoadingIndicator()Render all items.
@renderAllItems() if @renderItemsWhen an item is added, create a new view and insert it.
itemAdded: (item, collection, options) =>
@insertView item, @renderItem(item), options.atWhen an item is removed, remove the corresponding view from DOM and caches.
itemRemoved: (item) =>
@removeViewForItem itemWhen all items are resetted, render all anew.
itemsReset: =>
@renderAllItems() initFallback: ->
return unless @fallbackSelectorSet the $fallback property.
if $
@$fallback = @$ @fallbackSelector
else
@fallback = @find @fallbackSelectorListen for visible items changes.
@on 'visibilityChange', @toggleFallbackListen for sync events on the collection.
@listenTo @collection, 'syncStateChange', @toggleFallbackSet visibility initially.
@toggleFallback()Show fallback if no item is visible and the collection is synced.
toggleFallback: =>
visible = @visibleItems.length is 0 and (
if typeof @collection.isSynced is 'function'Collection is a SyncMachine.
@collection.isSynced()
elseAssume it is synced.
true
)
toggleElement (if $ then @$fallback else @fallback), visible initLoadingIndicator: ->The loading indicator only works for Collections which are SyncMachines.
return unless @loadingSelector and
typeof @collection.isSyncing is 'function'Set the $loading property.
if $
@$loading = @$ @loadingSelector
else
@loading = @find @loadingSelectorListen for sync events on the collection.
@listenTo @collection, 'syncStateChange', @toggleLoadingIndicatorSet visibility initially.
@toggleLoadingIndicator()
toggleLoadingIndicator: ->Only show the loading indicator if the collection is empty. Otherwise loading more items in order to append them would show the loading indicator. If you want the indicator to show up in this case, you need to overwrite this method to disable the check.
visible = @collection.length is 0 and @collection.isSyncing()
toggleElement (if $ then @$loading else @loading), visibleFilters only child item views from all current subviews.
getItemViews: ->
itemViews = {}
if @subviews.length > 0
for name, view of @subviewsByName when name.slice(0, 9) is 'itemView:'
itemViews[name.slice(9)] = view
itemViewsApplies a filter to the collection view. Expects an iterator function as first parameter which need to return true or false. Optional filter callback which is called to show/hide the view or mark it otherwise as filtered.
filter: (filterer, filterCallback) ->Save the filterer and filterCallback functions.
if typeof filterer is 'function' or filterer is null
@filterer = filterer
if typeof filterCallback is 'function' or filterCallback is null
@filterCallback = filterCallback
hasItemViews = do =>
if @subviews.length > 0
for name of @subviewsByName when name.slice(0, 9) is 'itemView:'
return true
falseShow/hide existing views.
if hasItemViews
for item, index in @collection.modelsApply filter to the item.
included = if typeof @filterer is 'function'
@filterer item, index
else
trueShow/hide the view accordingly.
view = @subview "itemView:#{item.cid}"A view has not been created for this item yet.
unless view
throw new Error 'CollectionView#filter: ' +
"no view found for #{item.cid}"Show/hide or mark the view accordingly.
@filterCallback view, includedUpdate visibleItems list, but do not trigger an event immediately.
@updateVisibleItems view.model, included, falseTrigger a combined visibilityChange event.
@trigger 'visibilityChange', @visibleItemsRender and insert all items.
renderAllItems: =>
items = @collection.modelsReset visible items.
@visibleItems = []Collect remaining views.
remainingViewsByCid = {}
for item in items
view = @subview "itemView:#{item.cid}"
if viewView remains.
remainingViewsByCid[item.cid] = viewRemove old views of items not longer in the list.
for own cid, view of @getItemViews() when cid not of remainingViewsByCidRemove the view.
@removeSubview "itemView:#{cid}"Re-insert remaining items; render and insert new items.
for item, index in itemsCheck if view was already created.
view = @subview "itemView:#{item.cid}"
if viewRe-insert the view.
@insertView item, view, index, false
elseCreate a new view, render and insert it.
@insertView item, @renderItem(item), indexIf no view was created, trigger visibilityChange event manually.
@trigger 'visibilityChange', @visibleItems if items.length is 0Instantiate and render an item using the viewsByCid hash as a cache.
renderItem: (item) ->Get the existing view.
view = @subview "itemView:#{item.cid}"Instantiate a new view if necessary.
unless view
view = @initItemView itemSave the view in the subviews.
@subview "itemView:#{item.cid}", viewRender in any case.
view.render()
viewReturns an instance of the view class. Override this method to use several item view constructors depending on the model type or data.
initItemView: (model) ->
if @itemView
new @itemView {autoRender: false, model}
else
throw new Error 'The CollectionView#itemView property ' +
'must be defined or the initItemView() must be overridden.'Inserts a view into the list at the proper position.
insertView: (item, view, position, enableAnimation = true) ->
enableAnimation = false if @animationDuration is 0Get the insertion offset if not given.
unless typeof position is 'number'
position = @collection.indexOf itemIs the item included in the filter?
included = if typeof @filterer is 'function'
@filterer item, position
else
trueGet the view’s top element.
elem = if $ then view.$el else view.elStart animation.
if included and enableAnimation
startAnimation elem, @useCssAnimation, @animationStartClassHide or mark the view if it’s filtered.
@filterCallback view, included if @filterer
length = @collection.lengthInsert the view into the list.
list = if $ then @$list else @list
insertView list, elem, position, length, @itemSelectorTell the view that it was added to its parent.
view.trigger 'addedToParent'Update the list of visible items, trigger a visibilityChange event.
@updateVisibleItems item, includedEnd animation.
if included and enableAnimation
if @useCssAnimationWait for DOM state change.
setTimeout (=> addClass elem, @animationEndClass), 0
elseFade the view in if it was made transparent before.
endAnimation elem, @animationDuration
viewRemove the view for an item.
removeViewForItem: (item) ->Remove item from visibleItems list, trigger a visibilityChange event.
@updateVisibleItems item, false
@removeSubview "itemView:#{item.cid}"Update visibleItems list and trigger a visibilityChanged event
if an item changed its visibility.
updateVisibleItems: (item, includedInFilter, triggerEvent = true) ->
visibilityChanged = false
visibleItemsIndex = utils.indexOf @visibleItems, item
includedInVisibleItems = visibleItemsIndex isnt -1
if includedInFilter and not includedInVisibleItemsAdd item to the visible items list.
@visibleItems.push item
visibilityChanged = true
else if not includedInFilter and includedInVisibleItemsRemove item from the visible items list.
@visibleItems.splice visibleItemsIndex, 1
visibilityChanged = trueTrigger a visibilityChange event if the visible items changed.
if visibilityChanged and triggerEvent
@trigger 'visibilityChange', @visibleItems
visibilityChanged dispose: ->
return if @disposedRemove jQuery objects, item view cache and visible items list.
properties = ['$list', '$fallback', '$loading', 'visibleItems']
delete this[prop] for prop in propertiesSelf-disposal.
super