2019-11-02 00:06:25 +01:00
const DEFAULTS = {
2019-10-25 09:15:16 +02:00
INITIAL _RENDER _COUNT : 80 ,
LOAD _COUNT : 20 ,
2020-02-12 08:07:21 +01:00
instances : new Map ( ) ,
2019-10-25 09:15:16 +02:00
} ;
2019-12-30 16:13:42 +01:00
exports . filter = ( value , list , opts ) => {
/ *
This is used by the main object ( see ` create ` ) ,
but we split it out to make it a bit easier
to test .
* /
2020-01-13 17:45:53 +01:00
if ( opts . filter . filterer ) {
return opts . filter . filterer ( list , value ) ;
}
2019-12-30 17:44:24 +01:00
const predicate = opts . filter . predicate ;
2019-12-30 16:13:42 +01:00
return list . filter ( function ( item ) {
2019-12-30 17:44:24 +01:00
return predicate ( item , value ) ;
2019-12-30 16:13:42 +01:00
} ) ;
} ;
2019-10-25 09:15:16 +02:00
// @params
// container: jQuery object to append to.
// list: The list of items to progressively append.
// opts: An object of random preferences.
exports . create = function ( $container , list , opts ) {
// this memoizes the results and will return a previously invoked
// instance's prototype.
2020-02-12 08:07:21 +01:00
if ( opts . name && DEFAULTS . instances . get ( opts . name ) ) {
2019-10-25 09:15:16 +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.
2020-02-12 08:07:21 +01:00
return DEFAULTS . instances . get ( opts . name )
2019-10-25 09:15:16 +02:00
// 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 ( ) ;
}
2019-11-02 00:06:25 +01:00
const meta = {
2019-10-25 09:15:16 +02:00
sorting _function : null ,
prop : null ,
2020-02-12 08:08:25 +01:00
sorting _functions : new Map ( ) ,
2020-02-12 08:09:01 +01:00
generic _sorting _functions : new Map ( ) ,
2019-10-25 09:15:16 +02:00
offset : 0 ,
list : list ,
filtered _list : list ,
2017-03-16 21:00:56 +01:00
} ;
2019-12-30 16:13:42 +01:00
function filter _list ( value ) {
meta . filtered _list = exports . filter ( value , meta . list , opts ) ;
}
2019-10-25 09:15:16 +02:00
if ( ! opts ) {
return ;
}
2020-01-13 17:45:53 +01:00
if ( opts . filter . predicate ) {
if ( typeof opts . filter . predicate !== 'function' ) {
blueslip . error ( 'Filter predicate function is missing.' ) ;
return ;
}
if ( opts . filter . filterer ) {
blueslip . error ( 'Filterer and predicate are mutually exclusive.' ) ;
return ;
}
} else {
if ( typeof opts . filter . filterer !== 'function' ) {
blueslip . error ( 'Filter filterer function is missing.' ) ;
return ;
}
2019-10-25 09:15:16 +02:00
}
2019-10-26 00:26:37 +02:00
const prototype = {
2019-10-25 09:15:16 +02:00
// 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 ;
}
2017-05-05 06:20:38 +02:00
2019-11-02 00:06:25 +01:00
const slice = meta . filtered _list . slice ( meta . offset , meta . offset + load _count ) ;
2017-03-16 21:00:56 +01:00
2020-01-15 15:05:44 +01:00
const finish = blueslip . start _timing ( 'list_render ' + opts . name ) ;
2020-02-08 06:17:38 +01:00
let html = "" ;
for ( const item of slice ) {
2019-11-02 00:06:25 +01:00
let _item = opts . modifier ( item ) ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
// if valid jQuery selection, attempt to grab all elements within
// and string them together into a giant outerHTML fragment.
if ( _item . constructor === jQuery ) {
_item = ( function ( $nodes ) {
2019-11-02 00:06:25 +01:00
let html = "" ;
2019-10-25 09:15:16 +02:00
$nodes . each ( function ( ) {
if ( this . nodeType === 1 ) {
html += this . outerHTML ;
}
} ) ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
return html ;
} ( _item ) ) ;
2017-03-16 21:00:56 +01:00
}
2019-10-25 09:15:16 +02:00
// if is a valid element, get the outerHTML.
if ( _item instanceof Element ) {
_item = _item . outerHTML ;
}
2017-05-05 06:20:38 +02:00
2020-02-08 06:17:38 +01:00
// append the HTML or nothing if corrupt (null, undef, etc.).
html += _item || "" ;
}
2019-10-25 09:15:16 +02:00
2020-01-15 15:05:44 +01:00
finish ( ) ;
2019-10-25 09:15:16 +02:00
$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 ( ) {
this . clear ( ) ;
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.
2020-02-12 01:35:16 +01:00
data : function ( ... args ) {
2019-10-25 09:15:16 +02:00
// if no args are provided then just return the existing data.
// this interface is similar to how many jQuery functions operate,
// where a call to the method without data returns the existing data.
2020-02-12 01:35:16 +01:00
if ( args . length === 0 ) {
2019-10-25 09:15:16 +02:00
return meta . list ;
}
2020-02-12 01:35:16 +01:00
const [ data ] = args ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
if ( Array . isArray ( data ) ) {
meta . list = data ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
if ( opts . filter && opts . filter . element ) {
2019-11-02 00:06:25 +01:00
const value = $ ( opts . filter . element ) . val ( ) . toLocaleLowerCase ( ) ;
2019-12-30 16:13:42 +01:00
filter _list ( value ) ;
2019-10-25 09:15:16 +02:00
}
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
prototype . clear ( ) ;
2017-03-16 21:00:56 +01:00
return this ;
2019-10-25 09:15:16 +02:00
}
2017-10-09 00:48:13 +02:00
2019-10-25 09:15:16 +02:00
blueslip . warn ( "The data object provided to the progressive" +
" list render is invalid" ) ;
return this ;
} ,
clear : function ( ) {
$container . html ( "" ) ;
meta . offset = 0 ;
return this ;
} ,
// 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 ;
}
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
return this ;
} ,
2017-05-05 06:20:38 +02:00
2019-10-25 09:15:16 +02:00
set _opts : function ( new _opts ) {
if ( opts ) {
opts = new _opts ;
}
2017-05-05 06:20:38 +02:00
2019-10-25 09:15:16 +02:00
return this ;
} ,
reverse : function ( ) {
meta . filtered _list . reverse ( ) ;
prototype . init ( ) ;
return this ;
} ,
// 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 , 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 */
2020-02-12 08:09:01 +01:00
meta . sorting _function = meta . generic _sorting _functions . get ( sorting _function ) ( prop ) ;
2019-10-25 09:15:16 +02:00
} else {
2020-02-12 08:08:25 +01:00
meta . sorting _function = meta . sorting _functions . get ( sorting _function ) ;
2017-03-16 21:00:56 +01:00
}
2019-10-25 09:15:16 +02:00
}
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
// we do not want to sort if we are just looking to reverse
// by calling with no sorting_function
if ( meta . sorting _function ) {
meta . filtered _list = meta . filtered _list . sort ( meta . sorting _function ) ;
}
2017-05-05 06:20:38 +02:00
2019-10-25 09:15:16 +02:00
if ( ! do _not _display ) {
// clear and re-initialize the list with the newly filtered subset
// of items.
prototype . init ( ) ;
2017-05-05 06:20:38 +02:00
2019-10-25 09:15:16 +02:00
if ( opts . filter . onupdate ) {
opts . filter . onupdate ( ) ;
}
}
} ,
add _sort _function : function ( name , sorting _function ) {
2020-02-12 08:08:25 +01:00
meta . sorting _functions . set ( name , sorting _function ) ;
2019-10-25 09:15:16 +02:00
} ,
// 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 ) {
2020-02-12 08:09:01 +01:00
meta . generic _sorting _functions . set ( name , sorting _function ) ;
2019-10-25 09:15:16 +02:00
} ,
remove _sort : function ( ) {
meta . sorting _function = false ;
} ,
// this sets the events given the particular arguments assigned in
// the container and opts.
_ _set _events : function ( ) {
2019-11-02 00:06:25 +01:00
let $nearestScrollingContainer = $container ;
2019-10-25 09:15:16 +02:00
while ( $nearestScrollingContainer . length ) {
if ( $nearestScrollingContainer . is ( "body, html" ) ) {
blueslip . warn ( "Please wrap progressive scrolling lists in an element with 'max-height' attribute. Error found in:\n" + blueslip . preview _node ( $container ) ) ;
break ;
2017-05-05 06:20:38 +02:00
}
2019-10-25 09:15:16 +02:00
if ( $nearestScrollingContainer . css ( "max-height" ) !== "none" ) {
break ;
2017-09-28 23:49:58 +02:00
}
2019-10-25 09:15:16 +02:00
$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-11-01 21:03:08 +01:00
}
2019-10-25 09:15:16 +02:00
} ) ;
if ( opts . filter . element ) {
opts . filter . element . on ( opts . filter . event || "input" , function ( ) {
2019-11-02 00:06:25 +01:00
const self = this ;
const value = self . value . toLocaleLowerCase ( ) ;
2019-10-25 09:15:16 +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 , true ) ;
2019-12-30 16:13:42 +01:00
filter _list ( value ) ;
2017-09-28 23:49:58 +02:00
2017-11-01 21:03:08 +01:00
// clear and re-initialize the list with the newly filtered subset
// of items.
prototype . init ( ) ;
2017-09-28 23:49:58 +02:00
2017-11-01 21:03:08 +01:00
if ( opts . filter . onupdate ) {
opts . filter . onupdate ( ) ;
2017-09-28 23:49:58 +02:00
}
2017-03-16 21:00:56 +01:00
} ) ;
2019-10-25 09:15:16 +02:00
}
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
return this ;
} ,
} ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
prototype . _ _set _events ( ) ;
2019-04-12 21:38:03 +02:00
2019-10-25 09:15:16 +02:00
// add built-in generic sort functions.
prototype . add _generic _sort _function ( "alphabetic" , function ( prop ) {
return function ( a , b ) {
// The conversion to uppercase helps make the sorting case insensitive.
2019-11-02 00:06:25 +01:00
const str1 = a [ prop ] . toUpperCase ( ) ;
const str2 = b [ prop ] . toUpperCase ( ) ;
2017-09-28 23:50:33 +02:00
2019-10-25 09:15:16 +02:00
if ( str1 === str2 ) {
return 0 ;
} else if ( str1 > str2 ) {
return 1 ;
}
2017-09-28 23:50:33 +02:00
2019-10-25 09:15:16 +02:00
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 ;
}
2017-09-28 23:50:33 +02:00
2019-10-25 09:15:16 +02:00
return - 1 ;
} ;
} ) ;
2017-09-28 23:50:33 +02:00
2019-10-25 09:15:16 +02:00
// Save the instance for potential future retrieval if a name is provided.
if ( opts . name ) {
2020-02-12 08:07:21 +01:00
DEFAULTS . instances . set ( opts . name , prototype ) ;
2019-10-25 09:15:16 +02:00
}
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
// Attach click handler to column heads for sorting rows accordingly
if ( opts . parent _container ) {
opts . parent _container . on ( "click" , "[data-sort]" , exports . handle _sort ) ;
}
2018-06-21 09:16:05 +02:00
2019-10-25 09:15:16 +02:00
return prototype ;
} ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
exports . get = function ( name ) {
2020-02-12 08:07:21 +01:00
return DEFAULTS . instances . get ( name ) || false ;
2019-10-25 09:15:16 +02:00
} ;
2017-03-16 21:00:56 +01:00
2019-10-25 09:15:16 +02:00
exports . handle _sort = function ( ) {
/ *
2018-06-21 06:58:19 +02:00
one would specify sort parameters like this :
- name => sort alphabetic .
- age => sort numeric .
2017-03-16 21:00:56 +01:00
2018-06-21 06:58:19 +02:00
you MUST specify the ` data-list-render ` in the ` .progressive-table-wrapper `
2017-09-28 23:51:34 +02:00
2018-06-21 06:58:19 +02:00
< div class = "progressive-table-wrapper" data - list - render = "some-list" >
< table >
2019-08-22 06:23:41 +02:00
< thead >
< th data - sort = "alphabetic" data - sort - prop = "name" > < / t h >
< th data - sort = "numeric" data - sort - prop = "age" > < / t h >
< / t h e a d >
2018-06-21 06:58:19 +02:00
< tbody > < / t b o d y >
< / t a b l e >
< / d i v >
* /
2019-11-02 00:06:25 +01:00
const $this = $ ( this ) ;
const sort _type = $this . data ( "sort" ) ;
const prop _name = $this . data ( "sort-prop" ) ;
const list _name = $this . closest ( ".progressive-table-wrapper" ) . data ( "list-render" ) ;
2019-10-25 09:15:16 +02:00
2019-11-02 00:06:25 +01:00
const list = exports . get ( list _name ) ;
2019-10-25 09:15:16 +02:00
if ( ! list ) {
blueslip . error ( "Error. This `.progressive-table-wrapper` has no `data-list-render` attribute." ) ;
return ;
}
if ( $this . hasClass ( "active" ) ) {
if ( ! $this . hasClass ( "descend" ) ) {
$this . addClass ( "descend" ) ;
} else {
$this . removeClass ( "descend" ) ;
2017-09-28 23:51:34 +02:00
}
2019-10-25 09:15:16 +02:00
list . reverse ( ) ;
// Table has already been sorted by this property; do not re-sort.
return ;
}
2017-10-22 19:58:08 +02:00
2019-10-25 09:15:16 +02:00
// if `prop_name` is defined, it will trigger the generic codepath,
// and not if it is undefined.
list . sort ( sort _type , prop _name ) ;
2017-10-22 19:58:08 +02:00
2019-10-25 09:15:16 +02:00
$this . siblings ( ".active" ) . removeClass ( "active" ) ;
$this . addClass ( "active" ) ;
} ;
2018-06-21 06:58:19 +02:00
2019-10-25 09:45:13 +02:00
window . list _render = exports ;