/ *
backbone - pageable 1.3 . 0
http : //github.com/wyuenho/backbone-pageable
Copyright ( c ) 2013 Jimmy Yuen Ho Wong
Licensed under the MIT @ license .
* /
( function ( factory ) {
// CommonJS
if ( typeof exports == "object" ) {
module . exports = factory ( require ( "underscore" ) , require ( "backbone" ) ) ;
}
// AMD
else if ( typeof define == "function" && define . amd ) {
define ( [ "underscore" , "backbone" ] , factory ) ;
}
// Browser
else if ( typeof _ !== "undefined" && typeof Backbone !== "undefined" ) {
var oldPageableCollection = Backbone . PageableCollection ;
var PageableCollection = Backbone . PageableCollection = factory ( _ , Backbone ) ;
/ * *
_ _BROWSER ONLY _ _
If you already have an object named ` PageableCollection ` attached to the
` Backbone ` module , you can use this to return a local reference to this
Backbone . PageableCollection class and reset the name
Backbone . PageableCollection to its previous definition .
// The left hand side gives you a reference to this
// Backbone.PageableCollection implementation, the right hand side
// resets Backbone.PageableCollection to your other
// Backbone.PageableCollection.
var PageableCollection = Backbone . PageableCollection . noConflict ( ) ;
@ static
@ member Backbone . PageableCollection
@ return { Backbone . PageableCollection }
* /
Backbone . PageableCollection . noConflict = function ( ) {
Backbone . PageableCollection = oldPageableCollection ;
return PageableCollection ;
} ;
}
} ( function ( _ , Backbone ) {
"use strict" ;
var _extend = _ . extend ;
var _omit = _ . omit ;
var _clone = _ . clone ;
var _each = _ . each ;
var _pick = _ . pick ;
var _contains = _ . contains ;
var _isEmpty = _ . isEmpty ;
var _pairs = _ . pairs ;
var _invert = _ . invert ;
var _isArray = _ . isArray ;
var _isFunction = _ . isFunction ;
var _isObject = _ . isObject ;
var _keys = _ . keys ;
var _isUndefined = _ . isUndefined ;
var _result = _ . result ;
var ceil = Math . ceil ;
var floor = Math . floor ;
var max = Math . max ;
var BBColProto = Backbone . Collection . prototype ;
function finiteInt ( val , name ) {
if ( ! _ . isNumber ( val ) || _ . isNaN ( val ) || ! _ . isFinite ( val ) || ~ ~ val !== val ) {
throw new TypeError ( "`" + name + "` must be a finite integer" ) ;
}
return val ;
}
function queryStringToParams ( qs ) {
var kvp , k , v , ls , params = { } , decode = decodeURIComponent ;
var kvps = qs . split ( '&' ) ;
for ( var i = 0 , l = kvps . length ; i < l ; i ++ ) {
var param = kvps [ i ] ;
kvp = param . split ( '=' ) , k = kvp [ 0 ] , v = kvp [ 1 ] || true ;
k = decode ( k ) , ls = params [ k ] ;
if ( _isArray ( ls ) ) ls . push ( v ) ;
else if ( ls ) params [ k ] = [ ls , v ] ;
else params [ k ] = v ;
}
return params ;
}
var PARAM _TRIM _RE = /[\s'"]/g ;
var URL _TRIM _RE = /[<>\s'"]/g ;
/ * *
Drop - in replacement for Backbone . Collection . Supports server - side and
client - side pagination and sorting . Client - side mode also support fully
multi - directional synchronization of changes between pages .
@ class Backbone . PageableCollection
@ extends Backbone . Collection
* /
var PageableCollection = Backbone . Collection . extend ( {
/ * *
The container object to store all pagination states .
You can override the default state by extending this class or specifying
them in an ` options ` hash to the constructor .
@ property { Object } state
@ property { 0 | 1 } [ state . firstPage = 1 ] The first page index . Set to 0 if
your server API uses 0 - based indices . You should only override this value
during extension , initialization or reset by the server after
fetching . This value should be read only at other times .
@ property { number } [ state . lastPage = null ] The last page index . This value
is _ _read only _ _ and it ' s calculated based on whether ` firstPage ` is 0 or
1 , during bootstrapping , fetching and resetting . Please don ' t change this
value under any circumstances .
@ property { number } [ state . currentPage = null ] The current page index . You
should only override this value during extension , initialization or reset
by the server after fetching . This value should be read only at other
times . Can be a 0 - based or 1 - based index , depending on whether
` firstPage ` is 0 or 1. If left as default , it will be set to ` firstPage `
on initialization .
@ property { number } [ state . pageSize = 25 ] How many records to show per
page . This value is _ _read only _ _ after initialization , if you want to
change the page size after initialization , you must call # setPageSize .
@ property { number } [ state . totalPages = null ] How many pages there are . This
value is _ _read only _ _ and it is calculated from ` totalRecords ` .
@ property { number } [ state . totalRecords = null ] How many records there
are . This value is _ _required _ _ under server mode . This value is optional
for client mode as the number will be the same as the number of models
during bootstrapping and during fetching , either supplied by the server
in the metadata , or calculated from the size of the response .
@ property { string } [ state . sortKey = null ] The model attribute to use for
sorting .
@ property { - 1 | 0 | 1 } [ state . order = - 1 ] The order to use for sorting . Specify
- 1 for ascending order or 1 for descending order . If 0 , no client side
sorting will be done and the order query parameter will not be sent to
the server during a fetch .
* /
state : {
firstPage : 1 ,
lastPage : null ,
currentPage : null ,
pageSize : 25 ,
totalPages : null ,
totalRecords : null ,
sortKey : null ,
order : - 1
} ,
/ * *
@ property { "server" | "client" | "infinite" } [ mode = "server" ] The mode of
operations for this collection . ` "server" ` paginates on the server - side ,
` "client" ` paginates on the client - side and ` "infinite" ` paginates on the
server - side for APIs that do not support ` totalRecords ` .
* /
mode : "server" ,
/ * *
A translation map to convert Backbone . PageableCollection state attributes
to the query parameters accepted by your server API .
You can override the default state by extending this class or specifying
them in ` options.queryParams ` object hash to the constructor .
@ property { Object } queryParams
@ property { string } [ queryParams . currentPage = "page" ]
@ property { string } [ queryParams . pageSize = "per_page" ]
@ property { string } [ queryParams . totalPages = "total_pages" ]
@ property { string } [ queryParams . totalRecords = "total_entries" ]
@ property { string } [ queryParams . sortKey = "sort_by" ]
@ property { string } [ queryParams . order = "order" ]
@ property { string } [ queryParams . directions = { "-1" : "asc" , "1" : "desc" } ] A
map for translating a Backbone . PageableCollection # state . order constant to
the ones your server API accepts .
* /
queryParams : {
currentPage : "page" ,
pageSize : "per_page" ,
totalPages : "total_pages" ,
totalRecords : "total_entries" ,
sortKey : "sort_by" ,
order : "order" ,
directions : {
"-1" : "asc" ,
"1" : "desc"
}
} ,
/ * *
_ _CLIENT MODE ONLY _ _
This collection is the internal storage for the bootstrapped or fetched
models . You can use this if you want to operate on all the pages .
@ property { Backbone . Collection } fullCollection
* /
/ * *
Given a list of models or model attributues , bootstraps the full
collection in client mode or infinite mode , or just the page you want in
server mode .
If you want to initialize a collection to a different state than the
default , you can specify them in ` options.state ` . Any state parameters
supplied will be merged with the default . If you want to change the
default mapping from # state keys to your server API ' s query parameter
names , you can specifiy an object hash in ` option.queryParams ` . Likewise ,
any mapping provided will be merged with the default . Lastly , all
Backbone . Collection constructor options are also accepted .
See :
- Backbone . PageableCollection # state
- Backbone . PageableCollection # queryParams
- [ Backbone . Collection # initialize ] ( http : //backbonejs.org/#Collection-constructor)
@ param { Array . < Object > } [ models ]
@ param { Object } [ options ]
@ param { function ( * , * ) : number } [ options . comparator ] If specified , this
comparator is set to the current page under server mode , or the # fullCollection
otherwise .
@ param { boolean } [ options . full ] If ` false ` and either a
` options.comparator ` or ` sortKey ` is defined , the comparator is attached
to the current page . Default is ` true ` under client or infinite mode and
the comparator will be attached to the # fullCollection .
@ param { Object } [ options . state ] The state attributes overriding the defaults .
@ param { string } [ options . state . sortKey ] The model attribute to use for
sorting . If specified instead of ` options.comparator ` , a comparator will
be automatically created using this value , and optionally a sorting order
specified in ` options.state.order ` . The comparator is then attached to
the new collection instance .
@ param { - 1 | 1 } [ options . state . order ] The order to use for sorting . Specify
- 1 for ascending order and 1 for descending order .
@ param { Object } [ options . queryParam ]
* /
constructor : function ( models , options ) {
Backbone . Collection . apply ( this , arguments ) ;
options = options || { } ;
var mode = this . mode = options . mode || this . mode || PageableProto . mode ;
var queryParams = _extend ( { } , PageableProto . queryParams , this . queryParams ,
options . queryParams || { } ) ;
queryParams . directions = _extend ( { } ,
PageableProto . queryParams . directions ,
this . queryParams . directions ,
queryParams . directions || { } ) ;
this . queryParams = queryParams ;
var state = this . state = _extend ( { } , PageableProto . state , this . state ,
options . state || { } ) ;
state . currentPage = state . currentPage == null ?
state . firstPage :
state . currentPage ;
if ( ! _isArray ( models ) ) models = models ? [ models ] : [ ] ;
if ( mode != "server" && state . totalRecords == null && ! _isEmpty ( models ) ) {
state . totalRecords = models . length ;
}
this . switchMode ( mode , _extend ( { fetch : false ,
resetState : false ,
models : models } , options ) ) ;
var comparator = options . comparator ;
if ( state . sortKey && ! comparator ) {
this . setSorting ( state . sortKey , state . order , options ) ;
}
if ( mode != "server" ) {
var fullCollection = this . fullCollection ;
if ( comparator && options . full ) {
delete this . comparator ;
fullCollection . comparator = comparator ;
}
if ( options . full ) fullCollection . sort ( ) ;
// make sure the models in the current page and full collection have the
// same references
if ( models && ! _isEmpty ( models ) ) {
this . getPage ( state . currentPage ) ;
models . splice . apply ( models , [ 0 , models . length ] . concat ( this . models ) ) ;
}
}
this . _initState = _clone ( this . state ) ;
} ,
/ * *
Makes a Backbone . Collection that contains all the pages .
@ private
@ param { Array . < Object | Backbone . Model > } models
@ param { Object } options Options for Backbone . Collection constructor .
@ return { Backbone . Collection }
* /
_makeFullCollection : function ( models , options ) {
var properties = [ "url" , "model" , "sync" , "comparator" ] ;
var thisProto = this . constructor . prototype ;
var i , length , prop ;
var proto = { } ;
for ( i = 0 , length = properties . length ; i < length ; i ++ ) {
prop = properties [ i ] ;
if ( ! _isUndefined ( thisProto [ prop ] ) ) {
proto [ prop ] = thisProto [ prop ] ;
}
}
var fullCollection = new ( Backbone . Collection . extend ( proto ) ) ( models , options ) ;
for ( i = 0 , length = properties . length ; i < length ; i ++ ) {
prop = properties [ i ] ;
if ( this [ prop ] !== thisProto [ prop ] ) {
fullCollection [ prop ] = this [ prop ] ;
}
}
return fullCollection ;
} ,
/ * *
Factory method that returns a Backbone event handler that responses to
the ` add ` , ` remove ` , ` reset ` , and the ` sort ` events . The returned event
handler will synchronize the current page collection and the full
collection ' s models .
@ private
@ param { Backbone . PageableCollection } pageCol
@ param { Backbone . Collection } fullCol
@ return { function ( string , Backbone . Model , Backbone . Collection , Object ) }
Collection event handler
* /
_makeCollectionEventHandler : function ( pageCol , fullCol ) {
return function collectionEventHandler ( event , model , collection , options ) {
var handlers = pageCol . _handlers ;
_each ( _keys ( handlers ) , function ( event ) {
var handler = handlers [ event ] ;
pageCol . off ( event , handler ) ;
fullCol . off ( event , handler ) ;
} ) ;
var state = _clone ( pageCol . state ) ;
var firstPage = state . firstPage ;
var currentPage = firstPage === 0 ?
state . currentPage :
state . currentPage - 1 ;
var pageSize = state . pageSize ;
var pageStart = currentPage * pageSize , pageEnd = pageStart + pageSize ;
if ( event == "add" ) {
var pageIndex , fullIndex , addAt , colToAdd , options = options || { } ;
if ( collection == fullCol ) {
fullIndex = fullCol . indexOf ( model ) ;
if ( fullIndex >= pageStart && fullIndex < pageEnd ) {
colToAdd = pageCol ;
pageIndex = addAt = fullIndex - pageStart ;
}
}
else {
pageIndex = pageCol . indexOf ( model ) ;
fullIndex = pageStart + pageIndex ;
colToAdd = fullCol ;
var addAt = ! _isUndefined ( options . at ) ?
options . at + pageStart :
fullIndex ;
}
++ state . totalRecords ;
pageCol . state = pageCol . _checkState ( state ) ;
if ( colToAdd ) {
colToAdd . add ( model , _extend ( { } , options || { } , { at : addAt } ) ) ;
var modelToRemove = pageIndex >= pageSize ?
model :
! _isUndefined ( options . at ) && addAt < pageEnd && pageCol . length > pageSize ?
pageCol . at ( pageSize ) :
null ;
if ( modelToRemove ) {
var addHandlers = collection . _events . add || [ ] ,
popOptions = { onAdd : true } ;
if ( addHandlers . length ) {
var lastAddHandler = addHandlers [ addHandlers . length - 1 ] ;
var oldCallback = lastAddHandler . callback ;
lastAddHandler . callback = function ( ) {
try {
oldCallback . apply ( this , arguments ) ;
pageCol . remove ( modelToRemove , popOptions ) ;
}
finally {
lastAddHandler . callback = oldCallback ;
}
} ;
}
else pageCol . remove ( modelToRemove , popOptions ) ;
}
}
}
// remove the model from the other collection as well
if ( event == "remove" ) {
if ( ! options . onAdd ) {
// decrement totalRecords and update totalPages and lastPage
if ( ! -- state . totalRecords ) {
state . totalRecords = null ;
state . totalPages = null ;
}
else {
var totalPages = state . totalPages = ceil ( state . totalRecords / pageSize ) ;
state . lastPage = firstPage === 0 ? totalPages - 1 : totalPages ;
if ( state . currentPage > totalPages ) state . currentPage = state . lastPage ;
}
pageCol . state = pageCol . _checkState ( state ) ;
var nextModel , removedIndex = options . index ;
if ( collection == pageCol ) {
if ( nextModel = fullCol . at ( pageEnd ) ) pageCol . push ( nextModel ) ;
fullCol . remove ( model ) ;
}
else if ( removedIndex >= pageStart && removedIndex < pageEnd ) {
pageCol . remove ( model ) ;
nextModel = fullCol . at ( currentPage * ( pageSize + removedIndex ) ) ;
if ( nextModel ) pageCol . push ( nextModel ) ;
}
}
else delete options . onAdd ;
}
if ( event == "reset" ) {
options = collection ;
collection = model ;
// Reset that's not a result of getPage
if ( collection === pageCol && options . from == null &&
options . to == null ) {
var head = fullCol . models . slice ( 0 , pageStart ) ;
var tail = fullCol . models . slice ( pageStart + pageCol . models . length ) ;
fullCol . reset ( head . concat ( pageCol . models ) . concat ( tail ) , options ) ;
}
else if ( collection === fullCol ) {
if ( ! ( state . totalRecords = fullCol . models . length ) ) {
state . totalRecords = null ;
state . totalPages = null ;
}
if ( pageCol . mode == "client" ) {
state . lastPage = state . currentPage = state . firstPage ;
}
pageCol . state = pageCol . _checkState ( state ) ;
pageCol . reset ( fullCol . models . slice ( pageStart , pageEnd ) ,
_extend ( { } , options , { parse : false } ) ) ;
}
}
if ( event == "sort" ) {
options = collection ;
collection = model ;
if ( collection === fullCol ) {
pageCol . reset ( fullCol . models . slice ( pageStart , pageEnd ) ,
_extend ( { } , options , { parse : false } ) ) ;
}
}
_each ( _keys ( handlers ) , function ( event ) {
var handler = handlers [ event ] ;
_each ( [ pageCol , fullCol ] , function ( col ) {
col . on ( event , handler ) ;
var callbacks = col . _events [ event ] || [ ] ;
callbacks . unshift ( callbacks . pop ( ) ) ;
} ) ;
} ) ;
} ;
} ,
/ * *
Sanity check this collection ' s pagination states . Only perform checks
when all the required pagination state values are defined and not null .
If ` totalPages ` is undefined or null , it is set to ` totalRecords ` /
` pageSize ` . ` lastPage ` is set according to whether ` firstPage ` is 0 or 1
when no error occurs .
@ private
@ throws { TypeError } If ` totalRecords ` , ` pageSize ` , ` currentPage ` or
` firstPage ` is not a finite integer .
@ throws { RangeError } If ` pageSize ` , ` currentPage ` or ` firstPage ` is out
of bounds .
@ return { Object } Returns the ` state ` object if no error was found .
* /
_checkState : function ( state ) {
var mode = this . mode ;
var links = this . links ;
var totalRecords = state . totalRecords ;
var pageSize = state . pageSize ;
var currentPage = state . currentPage ;
var firstPage = state . firstPage ;
var totalPages = state . totalPages ;
if ( totalRecords != null && pageSize != null && currentPage != null &&
firstPage != null && ( mode == "infinite" ? links : true ) ) {
totalRecords = finiteInt ( totalRecords , "totalRecords" ) ;
pageSize = finiteInt ( pageSize , "pageSize" ) ;
currentPage = finiteInt ( currentPage , "currentPage" ) ;
firstPage = finiteInt ( firstPage , "firstPage" ) ;
if ( pageSize < 1 ) {
throw new RangeError ( "`pageSize` must be >= 1" ) ;
}
totalPages = state . totalPages = ceil ( totalRecords / pageSize ) ;
if ( firstPage < 0 || firstPage > 1 ) {
throw new RangeError ( "`firstPage must be 0 or 1`" ) ;
}
state . lastPage = firstPage === 0 ? max ( 0 , totalPages - 1 ) : totalPages ;
if ( mode == "infinite" ) {
if ( ! links [ currentPage + '' ] ) {
throw new RangeError ( "No link found for page " + currentPage ) ;
}
}
else if ( currentPage < firstPage ||
( totalPages > 0 &&
( firstPage ? currentPage > totalPages : currentPage >= totalPages ) ) ) {
throw new RangeError ( "`currentPage` must be firstPage <= currentPage " +
( firstPage ? ">" : ">=" ) +
" totalPages if " + firstPage + "-based. Got " +
currentPage + '.' ) ;
}
}
return state ;
} ,
/ * *
Change the page size of this collection .
For server mode operations , changing the page size will trigger a # fetch
and subsequently a ` reset ` event .
For client mode operations , changing the page size will ` reset ` the
current page by recalculating the current page boundary on the client
side .
If ` options.fetch ` is true , a fetch can be forced if the collection is in
client mode .
@ param { number } pageSize The new page size to set to # state .
@ param { Object } [ options ] { @ link # fetch } options .
@ param { boolean } [ options . fetch ] If ` true ` , force a fetch in client mode .
@ throws { TypeError } If ` pageSize ` is not a finite integer .
@ throws { RangeError } If ` pageSize ` is less than 1.
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
setPageSize : function ( pageSize , options ) {
pageSize = finiteInt ( pageSize , "pageSize" ) ;
options = options || { } ;
this . state = this . _checkState ( _extend ( { } , this . state , {
pageSize : pageSize ,
totalPages : ceil ( this . state . totalRecords / pageSize )
} ) ) ;
return this . getPage ( this . state . currentPage , options ) ;
} ,
/ * *
Switching between client , server and infinite mode .
If switching from client to server mode , the # fullCollection is emptied
first and then deleted and a fetch is immediately issued for the current
page from the server . Pass ` false ` to ` options.fetch ` to skip fetching .
If switching to infinite mode , and if ` options.models ` is given for an
array of models , # links will be populated with a URL per page , using the
default URL for this collection .
If switching from server to client mode , all of the pages are immediately
refetched . If you have too many pages , you can pass ` false ` to
` options.fetch ` to skip fetching .
If switching to any mode from infinite mode , the # links will be deleted .
@ param { "server" | "client" | "infinite" } [ mode ] The mode to switch to .
@ param { Object } [ options ]
@ param { boolean } [ options . fetch = true ] If ` false ` , no fetching is done .
@ param { boolean } [ options . resetState = true ] If 'false' , the state is not
reset , but checked for sanity instead .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this if ` options.fetch ` is ` false ` .
* /
switchMode : function ( mode , options ) {
if ( ! _contains ( [ "server" , "client" , "infinite" ] , mode ) ) {
throw new TypeError ( '`mode` must be one of "server", "client" or "infinite"' ) ;
}
options = options || { fetch : true , resetState : true } ;
var state = this . state = options . resetState ?
_clone ( this . _initState ) :
this . _checkState ( _extend ( { } , this . state ) ) ;
this . mode = mode ;
var self = this ;
var fullCollection = this . fullCollection ;
var handlers = this . _handlers = this . _handlers || { } , handler ;
if ( mode != "server" && ! fullCollection ) {
fullCollection = this . _makeFullCollection ( options . models || [ ] ) ;
fullCollection . pageableCollection = this ;
this . fullCollection = fullCollection ;
var allHandler = this . _makeCollectionEventHandler ( this , fullCollection ) ;
_each ( [ "add" , "remove" , "reset" , "sort" ] , function ( event ) {
handlers [ event ] = handler = _ . bind ( allHandler , { } , event ) ;
self . on ( event , handler ) ;
fullCollection . on ( event , handler ) ;
} ) ;
fullCollection . comparator = this . _fullComparator ;
}
else if ( mode == "server" && fullCollection ) {
_each ( _keys ( handlers ) , function ( event ) {
handler = handlers [ event ] ;
self . off ( event , handler ) ;
fullCollection . off ( event , handler ) ;
} ) ;
delete this . _handlers ;
this . _fullComparator = fullCollection . comparator ;
delete this . fullCollection ;
}
if ( mode == "infinite" ) {
var links = this . links = { } ;
var firstPage = state . firstPage ;
var totalPages = ceil ( state . totalRecords / state . pageSize ) ;
var lastPage = firstPage === 0 ? max ( 0 , totalPages - 1 ) : totalPages || firstPage ;
for ( var i = state . firstPage ; i <= lastPage ; i ++ ) {
links [ i ] = this . url ;
}
}
else if ( this . links ) delete this . links ;
return options . fetch ?
this . fetch ( _omit ( options , "fetch" , "resetState" ) ) :
this ;
} ,
/ * *
@ return { boolean } ` true ` if this collection can page backward , ` false `
otherwise .
* /
hasPrevious : function ( ) {
var state = this . state ;
var currentPage = state . currentPage ;
if ( this . mode != "infinite" ) return currentPage > state . firstPage ;
return ! ! this . links [ currentPage - 1 ] ;
} ,
/ * *
@ return { boolean } ` true ` if this collection can page forward , ` false `
otherwise .
* /
hasNext : function ( ) {
var state = this . state ;
var currentPage = this . state . currentPage ;
if ( this . mode != "infinite" ) return currentPage < state . lastPage ;
return ! ! this . links [ currentPage + 1 ] ;
} ,
/ * *
Fetch the first page in server mode , or reset the current page of this
collection to the first page in client or infinite mode .
@ param { Object } options { @ link # getPage } options .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
getFirstPage : function ( options ) {
return this . getPage ( "first" , options ) ;
} ,
/ * *
Fetch the previous page in server mode , or reset the current page of this
collection to the previous page in client or infinite mode .
@ param { Object } options { @ link # getPage } options .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
getPreviousPage : function ( options ) {
return this . getPage ( "prev" , options ) ;
} ,
/ * *
Fetch the next page in server mode , or reset the current page of this
collection to the next page in client mode .
@ param { Object } options { @ link # getPage } options .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
getNextPage : function ( options ) {
return this . getPage ( "next" , options ) ;
} ,
/ * *
Fetch the last page in server mode , or reset the current page of this
collection to the last page in client mode .
@ param { Object } options { @ link # getPage } options .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
getLastPage : function ( options ) {
return this . getPage ( "last" , options ) ;
} ,
/ * *
Given a page index , set # state . currentPage to that index . If this
collection is in server mode , fetch the page using the updated state ,
otherwise , reset the current page of this collection to the page
specified by ` index ` in client mode . If ` options.fetch ` is true , a fetch
can be forced in client mode before resetting the current page . Under
infinite mode , if the index is less than the current page , a reset is
done as in client mode . If the index is greater than the current page
number , a fetch is made with the results * * appended * * to
# fullCollection . The current page will then be reset after fetching .
@ param { number | string } index The page index to go to , or the page name to
look up from # links in infinite mode .
@ param { Object } [ options ] { @ link # fetch } options or
[ reset ] ( http : //backbonejs.org/#Collection-reset) options for client mode
when ` options.fetch ` is ` false ` .
@ param { boolean } [ options . fetch = false ] If true , force a { @ link # fetch } in
client mode .
@ throws { TypeError } If ` index ` is not a finite integer under server or
client mode , or does not yield a URL from # links under infinite mode .
@ throws { RangeError } If ` index ` is out of bounds .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
getPage : function ( index , options ) {
var mode = this . mode , fullCollection = this . fullCollection ;
options = options || { fetch : false } ;
var state = this . state ,
firstPage = state . firstPage ,
currentPage = state . currentPage ,
lastPage = state . lastPage ,
pageSize = state . pageSize ;
var pageNum = index ;
switch ( index ) {
case "first" : pageNum = firstPage ; break ;
case "prev" : pageNum = currentPage - 1 ; break ;
case "next" : pageNum = currentPage + 1 ; break ;
case "last" : pageNum = lastPage ; break ;
default : pageNum = finiteInt ( index , "index" ) ;
}
this . state = this . _checkState ( _extend ( { } , state , { currentPage : pageNum } ) ) ;
options . from = currentPage , options . to = pageNum ;
var pageStart = ( firstPage === 0 ? pageNum : pageNum - 1 ) * pageSize ;
var pageModels = fullCollection && fullCollection . length ?
fullCollection . models . slice ( pageStart , pageStart + pageSize ) :
[ ] ;
if ( ( mode == "client" || ( mode == "infinite" && ! _isEmpty ( pageModels ) ) ) &&
! options . fetch ) {
return this . reset ( pageModels , _omit ( options , "fetch" ) ) ;
}
if ( mode == "infinite" ) options . url = this . links [ pageNum ] ;
return this . fetch ( _omit ( options , "fetch" ) ) ;
} ,
/ * *
Fetch the page for the provided item offset in server mode , or reset the current page of this
collection to the page for the provided item offset in client mode .
@ param { Object } options { @ link # getPage } options .
@ chainable
@ return { XMLHttpRequest | Backbone . PageableCollection } The XMLHttpRequest
from fetch or this .
* /
getPageByOffset : function ( offset , options ) {
if ( offset < 0 ) {
throw new RangeError ( "`offset must be > 0`" ) ;
}
offset = finiteInt ( offset ) ;
var page = floor ( offset / this . state . pageSize ) ;
if ( this . state . firstPage !== 0 ) page ++ ;
if ( page > this . state . lastPage ) page = this . state . lastPage ;
return this . getPage ( page , options ) ;
} ,
/ * *
Overidden to make ` getPage ` compatible with Zepto .
@ param { string } method
@ param { Backbone . Model | Backbone . Collection } model
@ param { Object } [ options ]
@ return { XMLHttpRequest }
* /
sync : function ( method , model , options ) {
var self = this ;
if ( self . mode == "infinite" ) {
var success = options . success ;
var currentPage = self . state . currentPage ;
options . success = function ( resp , status , xhr ) {
var links = self . links ;
var newLinks = self . parseLinks ( resp , _extend ( { xhr : xhr } , options ) ) ;
if ( newLinks . first ) links [ self . state . firstPage ] = newLinks . first ;
if ( newLinks . prev ) links [ currentPage - 1 ] = newLinks . prev ;
if ( newLinks . next ) links [ currentPage + 1 ] = newLinks . next ;
if ( success ) success ( resp , status , xhr ) ;
} ;
}
return ( BBColProto . sync || Backbone . sync ) . call ( self , method , model , options ) ;
} ,
/ * *
Parse pagination links from the server response . Only valid under
infinite mode .
Given a response body and a XMLHttpRequest object , extract pagination
links from them for infinite paging .
This default implementation parses the RFC 5988 ` Link ` header and extract
3 links from it - ` first ` , ` prev ` , ` next ` . If a ` previous ` link is found ,
it will be found in the ` prev ` key in the returned object hash . Any
subclasses overriding this method _ _must _ _ return an object hash having
only the keys above . If ` first ` is missing , the collection ' s default URL
is assumed to be the ` first ` URL . If ` prev ` or ` next ` is missing , it is
assumed to be ` null ` . An empty object hash must be returned if there are
no links found . If either the response or the header contains information
pertaining to the total number of records on the server ,
# state . totalRecords must be set to that number . The default
implementation uses the ` last ` link from the header to calculate it .
@ param { * } resp The deserialized response body .
@ param { Object } [ options ]
@ param { XMLHttpRequest } [ options . xhr ] The XMLHttpRequest object for this
response .
@ return { Object }
* /
parseLinks : function ( resp , options ) {
var links = { } ;
var linkHeader = options . xhr . getResponseHeader ( "Link" ) ;
if ( linkHeader ) {
var relations = [ "first" , "prev" , "previous" , "next" , "last" ] ;
_each ( linkHeader . split ( "," ) , function ( linkValue ) {
var linkParts = linkValue . split ( ";" ) ;
var url = linkParts [ 0 ] . replace ( URL _TRIM _RE , '' ) ;
var params = linkParts . slice ( 1 ) ;
_each ( params , function ( param ) {
var paramParts = param . split ( "=" ) ;
var key = paramParts [ 0 ] . replace ( PARAM _TRIM _RE , '' ) ;
var value = paramParts [ 1 ] . replace ( PARAM _TRIM _RE , '' ) ;
if ( key == "rel" && _contains ( relations , value ) ) {
if ( value == "previous" ) links . prev = url ;
else links [ value ] = url ;
}
} ) ;
} ) ;
var last = links . last || '' , qsi , qs ;
if ( qs = ( qsi = last . indexOf ( '?' ) ) ? last . slice ( qsi + 1 ) : '' ) {
var params = queryStringToParams ( qs ) ;
var state = _clone ( this . state ) ;
var queryParams = this . queryParams ;
var pageSize = state . pageSize ;
var totalRecords = params [ queryParams . totalRecords ] * 1 ;
var pageNum = params [ queryParams . currentPage ] * 1 ;
var totalPages = params [ queryParams . totalPages ] ;
if ( ! totalRecords ) {
if ( pageNum ) totalRecords = ( state . firstPage === 0 ?
pageNum + 1 :
pageNum ) * pageSize ;
else if ( totalPages ) totalRecords = totalPages * pageSize ;
}
if ( totalRecords ) state . totalRecords = totalRecords ;
this . state = this . _checkState ( state ) ;
}
}
delete links . last ;
return links ;
} ,
/ * *
Parse server response data .
This default implementation assumes the response data is in one of two
structures :
[
{ } , // Your new pagination state
[ { } , ... ] // An array of JSON objects
]
Or ,
[ { } ] // An array of JSON objects
The first structure is the preferred form because the pagination states
may have been updated on the server side , sending them down again allows
this collection to update its states . If the response has a pagination
state object , it is checked for errors .
The second structure is the
[ Backbone . Collection # parse ] ( http : //backbonejs.org/#Collection-parse)
default .
* * Note : * * this method has been further simplified since 1.1 . 7. While
existing # parse implementations will continue to work , new code is
encouraged to override # parseState and # parseRecords instead .
@ param { Object } resp The deserialized response data from the server .
@ return { Array . < Object > } An array of model objects
* /
parse : function ( resp ) {
var newState = this . parseState ( resp , _clone ( this . queryParams ) , _clone ( this . state ) ) ;
if ( newState ) this . state = this . _checkState ( _extend ( { } , this . state , newState ) ) ;
return this . parseRecords ( resp ) ;
} ,
/ * *
Parse server response for server pagination state updates .
This default implementation first checks whether the response has any
state object as documented in # parse . If it exists , a state object is
returned by mapping the server state keys to this pageable collection
instance ' s query parameter keys using ` queryParams ` .
It is _ _NOT _ _ neccessary to return a full state object complete with all
the mappings defined in # queryParams . Any state object resulted is merged
with a copy of the current pageable collection state and checked for
sanity before actually updating . Most of the time , simply providing a new
` totalRecords ` value is enough to trigger a full pagination state
recalculation .
parseState : function ( resp , queryParams , state ) {
return { totalRecords : resp . total _entries } ;
}
This method _ _MUST _ _ return a new state object instead of directly
modifying the # state object . The behavior of directly modifying # state is
undefined .
@ param { Object } resp The deserialized response data from the server .
@ param { Object } queryParams A copy of # queryParams .
@ param { Object } state A copy of # state .
@ return { Object } A new ( partial ) state object .
* /
parseState : function ( resp , queryParams , state ) {
if ( resp && resp . length === 2 && _isObject ( resp [ 0 ] ) && _isArray ( resp [ 1 ] ) ) {
var newState = _clone ( state ) ;
var serverState = resp [ 0 ] ;
_each ( _pairs ( _omit ( queryParams , "directions" ) ) , function ( kvp ) {
var k = kvp [ 0 ] , v = kvp [ 1 ] ;
var serverVal = serverState [ v ] ;
if ( ! _isUndefined ( serverVal ) && ! _ . isNull ( serverVal ) ) newState [ k ] = serverState [ v ] ;
} ) ;
if ( serverState . order ) {
newState . order = _invert ( queryParams . directions ) [ serverState . order ] * 1 ;
}
return newState ;
}
} ,
/ * *
Parse server response for an array of model objects .
This default implementation first checks whether the response has any
state object as documented in # parse . If it exists , the array of model
objects is assumed to be the second element , otherwise the entire
response is returned directly .
@ param { Object } resp The deserialized response data from the server .
@ return { Array . < Object > } An array of model objects
* /
parseRecords : function ( resp ) {
if ( resp && resp . length === 2 && _isObject ( resp [ 0 ] ) && _isArray ( resp [ 1 ] ) ) {
return resp [ 1 ] ;
}
return resp ;
} ,
/ * *
Fetch a page from the server in server mode , or all the pages in client
mode . Under infinite mode , the current page is refetched by default and
then reset .
The query string is constructed by translating the current pagination
state to your server API query parameter using # queryParams . The current
page will reset after fetch .
@ param { Object } [ options ] Accepts all
[ Backbone . Collection # fetch ] ( http : //backbonejs.org/#Collection-fetch)
options .
@ return { XMLHttpRequest }
* /
fetch : function ( options ) {
options = options || { } ;
var state = this . _checkState ( this . state ) ;
var mode = this . mode ;
if ( mode == "infinite" && ! options . url ) {
options . url = this . links [ state . currentPage ] ;
}
var data = options . data || { } ;
// dedup query params
var url = _result ( options , "url" ) || _result ( this , "url" ) || '' ;
var qsi = url . indexOf ( '?' ) ;
if ( qsi != - 1 ) {
_extend ( data , queryStringToParams ( url . slice ( qsi + 1 ) ) ) ;
url = url . slice ( 0 , qsi ) ;
}
options . url = url ;
options . data = data ;
// map params except directions
var queryParams = this . mode == "client" ?
_pick ( this . queryParams , "sortKey" , "order" ) :
_omit ( _pick ( this . queryParams , _keys ( PageableProto . queryParams ) ) ,
"directions" ) ;
var i , kvp , k , v , kvps = _pairs ( queryParams ) , thisCopy = _clone ( this ) ;
for ( i = 0 ; i < kvps . length ; i ++ ) {
kvp = kvps [ i ] , k = kvp [ 0 ] , v = kvp [ 1 ] ;
v = _isFunction ( v ) ? v . call ( thisCopy ) : v ;
if ( state [ k ] != null && v != null ) {
data [ v ] = state [ k ] ;
}
}
// fix up sorting parameters
if ( state . sortKey && state . order ) {
data [ queryParams . order ] = this . queryParams . directions [ state . order + "" ] ;
}
else if ( ! state . sortKey ) delete data [ queryParams . order ] ;
// map extra query parameters
var extraKvps = _pairs ( _omit ( this . queryParams ,
_keys ( PageableProto . queryParams ) ) ) ;
for ( i = 0 ; i < extraKvps . length ; i ++ ) {
kvp = extraKvps [ i ] ;
v = kvp [ 1 ] ;
v = _isFunction ( v ) ? v . call ( thisCopy ) : v ;
data [ kvp [ 0 ] ] = v ;
}
var fullCol = this . fullCollection , links = this . links ;
if ( mode != "server" ) {
var self = this ;
var success = options . success ;
options . success = function ( col , resp , opts ) {
// make sure the caller's intent is obeyed
opts = opts || { } ;
if ( _isUndefined ( options . silent ) ) delete opts . silent ;
else opts . silent = options . silent ;
var models = col . models ;
var currentPage = state . currentPage ;
if ( mode == "client" ) fullCol . reset ( models , opts ) ;
else if ( links [ currentPage ] ) { // refetching a page
var pageSize = state . pageSize ;
var pageStart = ( state . firstPage === 0 ?
currentPage :
currentPage - 1 ) * pageSize ;
var fullModels = fullCol . models ;
var head = fullModels . slice ( 0 , pageStart ) ;
var tail = fullModels . slice ( pageStart + pageSize ) ;
fullModels = head . concat ( models ) . concat ( tail ) ;
var updateFunc = fullCol . set || fullCol . update ;
// Must silent update and trigger reset later because the event
// sychronization handler is temporarily taken out during either add
// or remove, which Collection#set does, so the pageable collection
// will be out of sync if not silenced because adding will trigger
// the sychonization event handler
updateFunc . call ( fullCol , fullModels , _extend ( { silent : true } , opts ) ) ;
fullCol . trigger ( "reset" , fullCol , opts ) ;
}
// fetching new page
else fullCol . add ( models , _extend ( { at : fullCol . length } , opts ) ) ;
if ( success ) success ( col , resp , opts ) ;
} ;
// silent the first reset from backbone
return BBColProto . fetch . call ( self , _extend ( { } , options , { silent : true } ) ) ;
}
return BBColProto . fetch . call ( this , options ) ;
} ,
/ * *
Convenient method for making a ` comparator ` sorted by a model attribute
identified by ` sortKey ` and ordered by ` order ` .
Like a Backbone . Collection , a Backbone . PageableCollection will maintain
the _ _current page _ _ in sorted order on the client side if a ` comparator `
is attached to it . If the collection is in client mode , you can attach a
comparator to # fullCollection to have all the pages reflect the global
sorting order by specifying an option ` full ` to ` true ` . You _ _must _ _ call
` sort ` manually or # fullCollection . sort after calling this method to
force a resort .
While you can use this method to sort the current page in server mode ,
the sorting order may not reflect the global sorting order due to the
additions or removals of the records on the server since the last
fetch . If you want the most updated page in a global sorting order , it is
recommended that you set # state . sortKey and optionally # state . order , and
then call # fetch .
@ protected
@ param { string } [ sortKey = this . state . sortKey ] See ` state.sortKey ` .
@ param { number } [ order = this . state . order ] See ` state.order ` .
See [ Backbone . Collection . comparator ] ( http : //backbonejs.org/#Collection-comparator).
* /
_makeComparator : function ( sortKey , order ) {
var state = this . state ;
sortKey = sortKey || state . sortKey ;
order = order || state . order ;
if ( ! sortKey || ! order ) return ;
return function ( left , right ) {
var l = left . get ( sortKey ) , r = right . get ( sortKey ) , t ;
if ( order === 1 ) t = l , l = r , r = t ;
if ( l === r ) return 0 ;
else if ( l < r ) return - 1 ;
return 1 ;
} ;
} ,
/ * *
Adjusts the sorting for this pageable collection .
Given a ` sortKey ` and an ` order ` , sets ` state.sortKey ` and
` state.order ` . A comparator can be applied on the client side to sort in
the order defined if ` options.side ` is ` "client" ` . By default the
comparator is applied to the # fullCollection . Set ` options.full ` to
` false ` to apply a comparator to the current page under any mode . Setting
` sortKey ` to ` null ` removes the comparator from both the current page and
the full collection .
@ chainable
@ param { string } sortKey See ` state.sortKey ` .
@ param { number } [ order = this . state . order ] See ` state.order ` .
@ param { Object } [ options ]
@ param { "server" | "client" } [ options . side ] By default , ` "client" ` if
` mode ` is ` "client" ` , ` "server" ` otherwise .
@ param { boolean } [ options . full = true ]
* /
setSorting : function ( sortKey , order , options ) {
var state = this . state ;
state . sortKey = sortKey ;
state . order = order = order || state . order ;
var fullCollection = this . fullCollection ;
var delComp = false , delFullComp = false ;
if ( ! sortKey ) delComp = delFullComp = true ;
var mode = this . mode ;
options = _extend ( { side : mode == "client" ? mode : "server" , full : true } ,
options ) ;
var comparator = this . _makeComparator ( sortKey , order ) ;
var full = options . full , side = options . side ;
if ( side == "client" ) {
if ( full ) {
if ( fullCollection ) fullCollection . comparator = comparator ;
delComp = true ;
}
else {
this . comparator = comparator ;
delFullComp = true ;
}
}
else if ( side == "server" && ! full ) {
this . comparator = comparator ;
}
if ( delComp ) delete this . comparator ;
if ( delFullComp && fullCollection ) delete fullCollection . comparator ;
return this ;
}
} ) ;
var PageableProto = PageableCollection . prototype ;
return PageableCollection ;
} ) ) ;