2017-03-16 21:00:56 +01:00
var list _render = ( function ( ) {
var DEFAULTS = {
INITIAL _RENDER _COUNT : 80 ,
LOAD _COUNT : 20 ,
instances : { } ,
} ;
// @params
// container: jQuery object to append to.
// list: The list of items to progressively append.
// opts: An object of random preferences.
var func = function ( $container , list , opts ) {
// this memoizes the results and will return a previously invoked
// instance's prototype.
if ( opts . name && DEFAULTS . instances [ opts . name ] ) {
2017-05-05 06:20:38 +02:00
// the false flag here means "don't run `init`". This is because a
// user is likely reinitializing and will have put .init() afterwards.
// This happens when the same codepath is hit multiple times.
return DEFAULTS . instances [ opts . name ]
// sets the container to the new container in this prototype's args.
. set _container ( $container )
// sets the input to the new input in the args.
. set _opts ( opts )
. _ _set _events ( )
. data ( list )
. init ( ) ;
2017-03-16 21:00:56 +01:00
}
var meta = {
2017-09-28 23:49:58 +02:00
sorting _function : null ,
prop : null ,
sorting _functions : { } ,
generic _sorting _functions : { } ,
2017-03-16 21:00:56 +01:00
offset : 0 ,
listRenders : { } ,
2017-05-05 06:20:38 +02:00
list : list ,
2017-03-16 21:00:56 +01:00
filtered _list : list ,
2017-05-05 06:20:38 +02:00
filter _list : function ( value , callback ) {
this . filtered _list = this . list . filter ( function ( item ) {
if ( typeof callback === "function" ) {
return callback ( item , value ) ;
}
2017-08-25 03:30:54 +02:00
return ! ! ( item . toLocaleLowerCase ( ) . indexOf ( value ) >= 0 ) ;
2017-05-05 06:20:38 +02:00
} ) ;
} ,
} ;
2017-03-16 21:00:56 +01:00
if ( ! opts ) {
return ;
}
// we want to assume below that `opts.filter` exists, but may not necessarily
// have any defined specs.
if ( ! opts . filter ) {
opts . filter = { } ;
}
var prototype = {
// Reads the provided list (in the scope directly above)
// and renders the next block of messages automatically
// into the specified contianer.
render : function ( load _count ) {
load _count = load _count || opts . load _count || DEFAULTS . LOAD _COUNT ;
// Stop once the offset reaches the length of the original list.
if ( meta . offset >= meta . filtered _list . length ) {
return ;
}
var slice = meta . filtered _list . slice ( meta . offset , meta . offset + load _count ) ;
var html = _ . reduce ( slice , function ( acc , item ) {
var _item = opts . modifier ( item ) ;
2017-05-05 06:20:38 +02:00
// if valid jQuery selection, attempt to grab all elements within
// and string them together into a giant outerHTML fragment.
2017-03-16 21:00:56 +01:00
if ( _item . constructor === jQuery ) {
2017-05-05 06:20:38 +02:00
_item = ( function ( $nodes ) {
var html = "" ;
$nodes . each ( function ( ) {
if ( this . nodeType === 1 ) {
html += this . outerHTML ;
}
} ) ;
return html ;
} ( _item ) ) ;
2017-03-16 21:00:56 +01:00
}
// if is a valid element, get the outerHTML.
if ( _item instanceof Element ) {
_item = _item . outerHTML ;
}
// return the modified HTML or nothing if corrupt (null, undef, etc.).
return acc + ( _item || "" ) ;
} , "" ) ;
$container . append ( $ ( html ) ) ;
meta . offset += load _count ;
return this ;
} ,
// Fills the container with an initial batch of items.
// Needs to be enough to exceed the max height, so that a
// scrollable area is created.
init : function ( ) {
2017-05-05 06:20:38 +02:00
this . clear ( ) ;
2017-03-16 21:00:56 +01:00
this . render ( DEFAULTS . INITIAL _RENDER _COUNT ) ;
return this ;
} ,
filter : function ( map _function ) {
meta . filtered _list = meta . list ( map _function ) ;
} ,
// reset the data associated with a list. This is so that instead of
// initializing a new progressive list render instance, you can just
// update the data of an existing one.
data : function ( data ) {
if ( Array . isArray ( data ) ) {
meta . list = data ;
2017-05-05 06:20:38 +02:00
if ( opts . filter && opts . filter . element ) {
var value = $ ( opts . filter . element ) . val ( ) . toLocaleLowerCase ( ) ;
meta . filter _list ( value , opts . filter . callback ) ;
}
prototype . clear ( ) ;
2017-03-16 21:00:56 +01:00
return this ;
}
blueslip . warn ( "The data object provided to the progressive" +
" list render is invalid" ) ;
return this ;
} ,
clear : function ( ) {
$container . html ( "" ) ;
meta . offset = 0 ;
return this ;
} ,
2017-05-05 06:20:38 +02:00
// Let's imagine the following:
// list_render is initialized and becomes prototope A with scope A.
// list_render is re-initialized and becomes prototype A with scope A again.
// The issue is that when re-initializing, new variables could have been thrown
// in and old variables could be useless (eg. dead nodes), so we need to
// replace these with new copies if necessary.
set _container : function ( $new _container ) {
if ( $new _container ) {
$container = $new _container ;
}
return this ;
} ,
set _opts : function ( new _opts ) {
if ( opts ) {
opts = new _opts ;
}
return this ;
} ,
2017-09-28 23:49:58 +02:00
// the sorting function is either the function or string that calls the
// function to sort the list by. The prop is used for generic functions
// that can be called to sort with a particular prop.
// the `map` will normalize the values with a function you provide to make
// it easier to sort with.
// `do_not_display` will signal to not update the DOM, likely because in
// the next function it will be updated in the DOM.
sort : function ( sorting _function , prop , map , do _not _display ) {
meta . prop = prop ;
if ( typeof sorting _function === "function" ) {
meta . sorting _function = sorting _function ;
} else if ( typeof sorting _function === "string" ) {
if ( typeof prop === "string" ) {
/* eslint-disable max-len */
meta . sorting _function = meta . generic _sorting _functions [ sorting _function ] ( prop ) ;
} else {
meta . sorting _function = meta . sorting _functions [ sorting _function ] ;
}
}
if ( meta . sorting _function ) {
meta . filtered _list = meta . filtered _list . sort ( meta . sorting _function ) ;
if ( ! do _not _display ) {
// clear and re-initialize the list with the newly filtered subset
// of items.
prototype . init ( ) ;
if ( opts . filter . onupdate ) {
opts . filter . onupdate ( ) ;
}
}
}
} ,
add _sort _function : function ( name , sorting _function ) {
meta . sorting _functions [ name ] = sorting _function ;
} ,
// generic sorting functions are ones that will use a specified prop
// and perform a sort on it with the given sorting function.
add _generic _sort _function : function ( name , sorting _function ) {
meta . generic _sorting _functions [ name ] = sorting _function ;
} ,
remove _sort : function ( ) {
meta . sorting _function = false ;
} ,
2017-05-05 06:20:38 +02:00
// this sets the events given the particular arguments assigned in
// the container and opts.
_ _set _events : function ( ) {
var $nearestScrollingContainer = $container ;
while ( $nearestScrollingContainer . length ) {
if ( $nearestScrollingContainer . is ( "body, html" ) ) {
2017-06-03 19:32:41 +02:00
blueslip . warn ( "Please wrap progressive scrolling lists in an element with 'max-height' attribute. Error found in:\n" + blueslip . preview _node ( $container ) ) ;
2017-05-05 06:20:38 +02:00
break ;
2017-03-16 21:00:56 +01:00
}
2017-05-05 06:20:38 +02:00
if ( $nearestScrollingContainer . css ( "max-height" ) !== "none" ) {
break ;
}
$nearestScrollingContainer = $nearestScrollingContainer . parent ( ) ;
}
// on scroll of the nearest scrolling container, if it hits the bottom
// of the container then fetch a new block of items and render them.
$nearestScrollingContainer . scroll ( function ( ) {
if ( this . scrollHeight - ( this . scrollTop + this . clientHeight ) < 10 ) {
prototype . render ( ) ;
}
2017-03-16 21:00:56 +01:00
} ) ;
2017-05-05 06:20:38 +02:00
if ( opts . filter . element ) {
opts . filter . element . on ( opts . filter . event || "input" , function ( ) {
var self = this ;
var value = self . value . toLocaleLowerCase ( ) ;
2017-09-28 23:49:58 +02:00
// run the sort algorithm that was used last, which is done
// by passing `undefined` -- which will make it use the params
// from the last sort.
// it will then also not run an update in the DOM (because we
// pass `true`), because it will update regardless below at
// `prototype.init()`.
prototype . sort ( undefined , meta . prop , undefined , true ) ;
2017-05-05 06:20:38 +02:00
meta . filter _list ( value , opts . filter . callback ) ;
// clear and re-initialize the list with the newly filtered subset
// of items.
2017-09-28 23:48:51 +02:00
prototype . init ( ) ;
2017-09-15 23:01:36 +02:00
if ( opts . filter . onupdate ) {
opts . filter . onupdate ( ) ;
}
2017-05-05 06:20:38 +02:00
} ) ;
}
return this ;
} ,
} ;
prototype . _ _set _events ( ) ;
2017-03-16 21:00:56 +01:00
2017-09-28 23:50:33 +02:00
// add built-in generic sort functions.
prototype . add _generic _sort _function ( "alphabetic" , function ( prop ) {
return function ( a , b ) {
if ( a [ prop ] > b [ prop ] ) {
return 1 ;
} else if ( a [ prop ] === b [ prop ] ) {
return 0 ;
}
return - 1 ;
} ;
} ) ;
prototype . add _generic _sort _function ( "numeric" , function ( prop ) {
return function ( a , b ) {
if ( parseFloat ( a [ prop ] ) > parseFloat ( b [ prop ] ) ) {
return 1 ;
} else if ( parseFloat ( a [ prop ] ) === parseFloat ( b [ prop ] ) ) {
return 0 ;
}
return - 1 ;
} ;
} ) ;
2017-03-16 21:00:56 +01:00
// Save the instance for potential future retrieval if a name is provided.
if ( opts . name ) {
DEFAULTS . instances [ opts . name ] = prototype ;
}
return prototype ;
} ;
func . get = function ( name ) {
return DEFAULTS . instances [ name ] || false ;
} ;
// this can delete list render issues and free up memory if needed.
func . delete = function ( name ) {
if ( DEFAULTS . instances [ name ] ) {
delete DEFAULTS . instances [ name ] ;
return true ;
}
blueslip . warn ( "The progressive list render instance with the name '" +
name + "' does not exist." ) ;
return false ;
} ;
return func ;
} ( ) ) ;
if ( typeof module !== 'undefined' ) {
module . exports = list _render ;
}