mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
The list_render class "list" prop was immutable so when the data prop would be updated it would not appropriately update the data inside the primary list for filtering. This commit also fixes an issue where if a jQuery selection was passed in, all the nodes rather than just the first get copied over.
240 lines
8.5 KiB
JavaScript
240 lines
8.5 KiB
JavaScript
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]) {
|
|
// 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();
|
|
}
|
|
|
|
var meta = {
|
|
offset: 0,
|
|
listRenders: {},
|
|
list: list,
|
|
filtered_list: list,
|
|
|
|
filter_list: function (value, callback) {
|
|
this.filtered_list = this.list.filter(function (item) {
|
|
if (typeof callback === "function") {
|
|
return callback(item, value);
|
|
}
|
|
|
|
return !!item.toLocaleLowerCase().match(value);
|
|
});
|
|
},
|
|
};
|
|
|
|
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);
|
|
|
|
// 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) {
|
|
var html = "";
|
|
$nodes.each(function () {
|
|
if (this.nodeType === 1) {
|
|
html += this.outerHTML;
|
|
}
|
|
});
|
|
|
|
return html;
|
|
}(_item));
|
|
}
|
|
|
|
// 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 () {
|
|
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.
|
|
data: function (data) {
|
|
if (Array.isArray(data)) {
|
|
meta.list = data;
|
|
|
|
if (opts.filter && opts.filter.element) {
|
|
var value = $(opts.filter.element).val().toLocaleLowerCase();
|
|
meta.filter_list(value, opts.filter.callback);
|
|
}
|
|
|
|
prototype.clear();
|
|
|
|
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;
|
|
},
|
|
|
|
// 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;
|
|
},
|
|
|
|
// 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")) {
|
|
blueslip.warn("Please wrap progressive scrolling lists in an element with 'max-height' attribute. Error found in:\n" + util.preview_node($container));
|
|
break;
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
if (opts.filter.element) {
|
|
opts.filter.element.on(opts.filter.event || "input", function () {
|
|
var self = this;
|
|
var value = self.value.toLocaleLowerCase();
|
|
|
|
meta.filter_list(value, opts.filter.callback);
|
|
|
|
// clear and re-initialize the list with the newly filtered subset
|
|
// of items.
|
|
prototype.clear().init();
|
|
});
|
|
}
|
|
|
|
return this;
|
|
},
|
|
};
|
|
|
|
prototype.__set_events();
|
|
|
|
// 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;
|
|
}
|