mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 21:13:36 +00:00
This commit adds the support to select multiple dropdown items by inheriting dropdown list widget and overriding some of it's properties. The parameters that can be passed along with it are- - widget_name: The desired name of the widget. - data: The data that needs to be populated as dropdown items. - default_text: The default text to be rendered when none of the items is selected. - on_update: Function to trigger once the filter button is pressed. - on_close: Function to trigger once the dropdown is successfully closed after filtering. - value: The default value that is initially selected by user. - limit: The maximum number of dropdown items to display on button text. This widget can later be implemented in recent topic view to replace the several ellipses filter button and also within the organisation user's page to quickly sort the users list according to their org role.
927 lines
27 KiB
JavaScript
927 lines
27 KiB
JavaScript
"use strict";
|
|
|
|
const {strict: assert} = require("assert");
|
|
|
|
const {mock_esm, mock_jquery, set_global, zrequire} = require("../zjsunit/namespace");
|
|
const {run_test} = require("../zjsunit/test");
|
|
const blueslip = require("../zjsunit/zblueslip");
|
|
|
|
// We need these stubs to get by instanceof checks.
|
|
// The ListWidget library allows you to insert objects
|
|
// that are either jQuery, Element, or just raw HTML
|
|
// strings. We initially test with raw strings.
|
|
const ui = mock_esm("../../static/js/ui");
|
|
set_global("Element", () => {});
|
|
|
|
// We only need very simple jQuery wrappers for when the
|
|
|
|
// "real" code wraps html or sets up click handlers.
|
|
// We'll simulate most other objects ourselves.
|
|
mock_jquery((arg) => {
|
|
if (arg.to_jquery) {
|
|
return arg.to_jquery();
|
|
}
|
|
|
|
return {
|
|
replace: (regex, string) => {
|
|
arg = arg.replace(regex, string);
|
|
},
|
|
html: () => arg,
|
|
};
|
|
});
|
|
|
|
const ListWidget = zrequire("list_widget");
|
|
|
|
// We build objects here that simulate jQuery containers.
|
|
// The main thing to do at first is simulate that our
|
|
// scroll container is the nearest ancestor to our main
|
|
// container that has a max-height attribute, and then
|
|
// the scroll container will have a scroll event attached to
|
|
// it. This is a good time to read set_up_event_handlers
|
|
// in the real code.
|
|
|
|
function make_container() {
|
|
const container = {};
|
|
|
|
container.length = () => 1;
|
|
container.is = () => false;
|
|
container.css = (prop) => {
|
|
assert.equal(prop, "max-height");
|
|
return "none";
|
|
};
|
|
|
|
// Make our append function just set a field we can
|
|
// check in our tests.
|
|
container.append = (data) => {
|
|
container.appended_data = data;
|
|
};
|
|
|
|
return container;
|
|
}
|
|
|
|
function make_scroll_container() {
|
|
const scroll_container = {};
|
|
|
|
scroll_container.cleared = false;
|
|
|
|
// Capture the scroll callback so we can call it in
|
|
// our tests.
|
|
scroll_container.on = (ev, f) => {
|
|
assert.equal(ev, "scroll.list_widget_container");
|
|
scroll_container.call_scroll = () => {
|
|
f.call(scroll_container);
|
|
};
|
|
};
|
|
|
|
scroll_container.off = (ev) => {
|
|
assert.equal(ev, "scroll.list_widget_container");
|
|
scroll_container.cleared = true;
|
|
};
|
|
|
|
return scroll_container;
|
|
}
|
|
|
|
function make_sort_container() {
|
|
const sort_container = {};
|
|
|
|
sort_container.cleared = false;
|
|
|
|
sort_container.on = (ev, sel, f) => {
|
|
assert.equal(ev, "click.list_widget_sort");
|
|
assert.equal(sel, "[data-sort]");
|
|
sort_container.f = f;
|
|
};
|
|
|
|
sort_container.off = (ev) => {
|
|
assert.equal(ev, "click.list_widget_sort");
|
|
sort_container.cleared = true;
|
|
};
|
|
|
|
return sort_container;
|
|
}
|
|
|
|
function make_filter_element() {
|
|
const element = {};
|
|
|
|
element.cleared = false;
|
|
|
|
element.on = (ev, f) => {
|
|
assert.equal(ev, "input.list_widget_filter");
|
|
element.f = f;
|
|
};
|
|
|
|
element.off = (ev) => {
|
|
assert.equal(ev, "input.list_widget_filter");
|
|
element.cleared = true;
|
|
};
|
|
|
|
return element;
|
|
}
|
|
|
|
function make_search_input() {
|
|
const $element = {};
|
|
|
|
// Allow ourselves to be wrapped by $(...) and
|
|
// return ourselves.
|
|
$element.to_jquery = () => $element;
|
|
|
|
$element.on = (event_name, f) => {
|
|
assert.equal(event_name, "input.list_widget_filter");
|
|
$element.simulate_input_event = () => {
|
|
const elem = {
|
|
value: $element.val(),
|
|
};
|
|
f.call(elem);
|
|
};
|
|
};
|
|
|
|
return $element;
|
|
}
|
|
|
|
function div(item) {
|
|
return "<div>" + item + "</div>";
|
|
}
|
|
|
|
run_test("scrolling", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
|
|
const items = [];
|
|
|
|
let get_scroll_element_called = false;
|
|
ui.get_scroll_element = (element) => {
|
|
get_scroll_element_called = true;
|
|
return element;
|
|
};
|
|
|
|
for (let i = 0; i < 200; i += 1) {
|
|
items.push("item " + i);
|
|
}
|
|
|
|
const opts = {
|
|
modifier: (item) => item,
|
|
simplebar_container: scroll_container,
|
|
};
|
|
|
|
container.html = (html) => {
|
|
assert.equal(html, "");
|
|
};
|
|
ListWidget.create(container, items, opts);
|
|
|
|
assert.deepEqual(container.appended_data.html(), items.slice(0, 80).join(""));
|
|
assert.equal(get_scroll_element_called, true);
|
|
|
|
// Set up our fake geometry so it forces a scroll action.
|
|
scroll_container.scrollTop = 180;
|
|
scroll_container.clientHeight = 100;
|
|
scroll_container.scrollHeight = 260;
|
|
|
|
// Scrolling gets the next two elements from the list into
|
|
// our widget.
|
|
scroll_container.call_scroll();
|
|
assert.deepEqual(container.appended_data.html(), items.slice(80, 100).join(""));
|
|
});
|
|
|
|
run_test("not_scrolling", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
|
|
const items = [];
|
|
|
|
let get_scroll_element_called = false;
|
|
ui.get_scroll_element = (element) => {
|
|
get_scroll_element_called = true;
|
|
return element;
|
|
};
|
|
|
|
let post_scroll__pre_render_callback_called = false;
|
|
const post_scroll__pre_render_callback = () => {
|
|
post_scroll__pre_render_callback_called = true;
|
|
};
|
|
|
|
let get_min_load_count_called = false;
|
|
const get_min_load_count = (offset, load_count) => {
|
|
get_min_load_count_called = true;
|
|
return load_count;
|
|
};
|
|
|
|
for (let i = 0; i < 200; i += 1) {
|
|
items.push("item " + i);
|
|
}
|
|
|
|
const opts = {
|
|
modifier: (item) => item,
|
|
simplebar_container: scroll_container,
|
|
is_scroll_position_for_render: () => false,
|
|
post_scroll__pre_render_callback,
|
|
get_min_load_count,
|
|
};
|
|
|
|
container.html = (html) => {
|
|
assert.equal(html, "");
|
|
};
|
|
ListWidget.create(container, items, opts);
|
|
|
|
assert.deepEqual(container.appended_data.html(), items.slice(0, 80).join(""));
|
|
assert.equal(get_scroll_element_called, true);
|
|
|
|
// Set up our fake geometry.
|
|
scroll_container.scrollTop = 180;
|
|
scroll_container.clientHeight = 100;
|
|
scroll_container.scrollHeight = 260;
|
|
|
|
// Since `should_render` is always false, no elements will be
|
|
// added regardless of scrolling.
|
|
scroll_container.call_scroll();
|
|
// appended_data remains the same.
|
|
assert.deepEqual(container.appended_data.html(), items.slice(0, 80).join(""));
|
|
assert.equal(post_scroll__pre_render_callback_called, true);
|
|
assert.equal(get_min_load_count_called, true);
|
|
});
|
|
|
|
run_test("filtering", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
|
|
const search_input = make_search_input();
|
|
|
|
const list = ["apple", "banana", "carrot", "dog", "egg", "fence", "grape"];
|
|
const opts = {
|
|
filter: {
|
|
element: search_input,
|
|
predicate: (item, value) => item.includes(value),
|
|
},
|
|
modifier: (item) => div(item),
|
|
simplebar_container: scroll_container,
|
|
};
|
|
|
|
container.html = (html) => {
|
|
assert.equal(html, "");
|
|
};
|
|
const widget = ListWidget.create(container, list, opts);
|
|
|
|
let expected_html =
|
|
"<div>apple</div>" +
|
|
"<div>banana</div>" +
|
|
"<div>carrot</div>" +
|
|
"<div>dog</div>" +
|
|
"<div>egg</div>" +
|
|
"<div>fence</div>" +
|
|
"<div>grape</div>";
|
|
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
|
|
// Filtering will pick out dog/egg/grape when we put "g"
|
|
// into our search input. (This uses the default filter, which
|
|
// is a glorified indexOf call.)
|
|
search_input.val = () => "g";
|
|
search_input.simulate_input_event();
|
|
assert.deepEqual(widget.get_current_list(), ["dog", "egg", "grape"]);
|
|
expected_html = "<div>dog</div><div>egg</div><div>grape</div>";
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
|
|
// We can insert new data into the widget.
|
|
const new_data = ["greta", "faye", "gary", "frank", "giraffe", "fox"];
|
|
|
|
widget.replace_list_data(new_data);
|
|
expected_html = "<div>greta</div><div>gary</div><div>giraffe</div>";
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
});
|
|
|
|
run_test("no filtering", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
container.html = () => {};
|
|
|
|
let callback_called = false;
|
|
// Opts does not require a filter key.
|
|
const opts = {
|
|
modifier: (item) => div(item),
|
|
simplebar_container: scroll_container,
|
|
callback_after_render: () => {
|
|
callback_called = true;
|
|
},
|
|
};
|
|
const widget = ListWidget.create(container, ["apple", "banana"], opts);
|
|
widget.render();
|
|
assert.deepEqual(callback_called, true);
|
|
|
|
const expected_html = "<div>apple</div><div>banana</div>";
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
});
|
|
|
|
function sort_button(opts) {
|
|
// The complications here are due to needing to find
|
|
// the list via complicated HTML assumptions. Also, we
|
|
// don't have any abstraction for the button and its
|
|
// siblings other than direct jQuery actions.
|
|
|
|
function data(sel) {
|
|
switch (sel) {
|
|
case "sort":
|
|
return opts.sort_type;
|
|
case "sort-prop":
|
|
return opts.prop_name;
|
|
default:
|
|
throw new Error("unknown selector: " + sel);
|
|
}
|
|
}
|
|
|
|
function lookup(sel, value) {
|
|
return (selector) => {
|
|
assert.equal(sel, selector);
|
|
return value;
|
|
};
|
|
}
|
|
|
|
const classList = new Set();
|
|
|
|
const button = {
|
|
data,
|
|
closest: lookup(".progressive-table-wrapper", {
|
|
data: lookup("list-widget", opts.list_name),
|
|
}),
|
|
addClass: (cls) => {
|
|
classList.add(cls);
|
|
},
|
|
hasClass: (cls) => classList.has(cls),
|
|
removeClass: (cls) => {
|
|
classList.delete(cls);
|
|
},
|
|
siblings: lookup(".active", {
|
|
removeClass: (cls) => {
|
|
assert.equal(cls, "active");
|
|
button.siblings_deactivated = true;
|
|
},
|
|
}),
|
|
siblings_deactivated: false,
|
|
to_jquery: () => button,
|
|
};
|
|
|
|
return button;
|
|
}
|
|
|
|
run_test("wire up filter element", () => {
|
|
const lst = ["alice", "JESSE", "moses", "scott", "Sean", "Xavier"];
|
|
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
const filter_element = make_filter_element();
|
|
|
|
// We don't care about what gets drawn initially.
|
|
container.html = () => {};
|
|
|
|
const opts = {
|
|
filter: {
|
|
filterer: (list, value) => list.filter((item) => item.toLowerCase().includes(value)),
|
|
element: filter_element,
|
|
},
|
|
modifier: (s) => "(" + s + ")",
|
|
simplebar_container: scroll_container,
|
|
};
|
|
|
|
ListWidget.create(container, lst, opts);
|
|
filter_element.f.apply({value: "se"});
|
|
assert.equal(container.appended_data.html(), "(JESSE)(moses)(Sean)");
|
|
});
|
|
|
|
run_test("sorting", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
const sort_container = make_sort_container();
|
|
|
|
let cleared;
|
|
container.html = (html) => {
|
|
assert.equal(html, "");
|
|
cleared = true;
|
|
};
|
|
|
|
const alice = {name: "alice", salary: 50};
|
|
const bob = {name: "Bob", salary: 40};
|
|
const cal = {name: "cal", salary: 30};
|
|
const dave = {name: "dave", salary: 25};
|
|
const ellen = {name: "ellen", salary: 95};
|
|
|
|
const list = [bob, ellen, dave, alice, cal];
|
|
|
|
const opts = {
|
|
name: "sorting-list",
|
|
parent_container: sort_container,
|
|
modifier: (item) => div(item.name) + div(item.salary),
|
|
filter: {
|
|
predicate: () => true,
|
|
},
|
|
simplebar_container: scroll_container,
|
|
};
|
|
|
|
function html_for(people) {
|
|
return people.map((item) => opts.modifier(item)).join("");
|
|
}
|
|
|
|
ListWidget.create(container, list, opts);
|
|
|
|
let button_opts;
|
|
let button;
|
|
let expected_html;
|
|
|
|
button_opts = {
|
|
sort_type: "alphabetic",
|
|
prop_name: "name",
|
|
list_name: "my-list",
|
|
active: false,
|
|
};
|
|
|
|
button = sort_button(button_opts);
|
|
|
|
sort_container.f.apply(button);
|
|
|
|
assert.ok(cleared);
|
|
assert.ok(button.siblings_deactivated);
|
|
|
|
expected_html = html_for([alice, bob, cal, dave, ellen]);
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
|
|
// Hit same button again to reverse the data.
|
|
cleared = false;
|
|
sort_container.f.apply(button);
|
|
assert.ok(cleared);
|
|
expected_html = html_for([ellen, dave, cal, bob, alice]);
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
assert.ok(button.hasClass("descend"));
|
|
|
|
// And then hit a third time to go back to the forward sort.
|
|
cleared = false;
|
|
sort_container.f.apply(button);
|
|
assert.ok(cleared);
|
|
expected_html = html_for([alice, bob, cal, dave, ellen]);
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
assert.ok(!button.hasClass("descend"));
|
|
|
|
// Now try a numeric sort.
|
|
button_opts = {
|
|
sort_type: "numeric",
|
|
prop_name: "salary",
|
|
list_name: "my-list",
|
|
active: false,
|
|
};
|
|
|
|
button = sort_button(button_opts);
|
|
|
|
cleared = false;
|
|
button.siblings_deactivated = false;
|
|
|
|
sort_container.f.apply(button);
|
|
|
|
assert.ok(cleared);
|
|
assert.ok(button.siblings_deactivated);
|
|
|
|
expected_html = html_for([dave, cal, bob, alice, ellen]);
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
|
|
// Hit same button again to reverse the numeric sort.
|
|
cleared = false;
|
|
sort_container.f.apply(button);
|
|
assert.ok(cleared);
|
|
expected_html = html_for([ellen, alice, bob, cal, dave]);
|
|
assert.deepEqual(container.appended_data.html(), expected_html);
|
|
assert.ok(button.hasClass("descend"));
|
|
});
|
|
|
|
run_test("custom sort", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
container.html = () => {};
|
|
|
|
const n42 = {x: 6, y: 7};
|
|
const n43 = {x: 1, y: 43};
|
|
const n44 = {x: 4, y: 11};
|
|
|
|
const list = [n42, n43, n44];
|
|
|
|
function sort_by_x(a, b) {
|
|
return a.x - b.x;
|
|
}
|
|
|
|
function sort_by_product(a, b) {
|
|
return a.x * a.y - b.x * b.y;
|
|
}
|
|
|
|
ListWidget.create(container, list, {
|
|
name: "custom-sort-list",
|
|
modifier: (n) => "(" + n.x + ", " + n.y + ")",
|
|
sort_fields: {
|
|
product: sort_by_product,
|
|
x_value: sort_by_x,
|
|
},
|
|
init_sort: [sort_by_product],
|
|
simplebar_container: scroll_container,
|
|
});
|
|
|
|
assert.deepEqual(container.appended_data.html(), "(6, 7)(1, 43)(4, 11)");
|
|
|
|
const widget = ListWidget.get("custom-sort-list");
|
|
|
|
widget.sort("x_value");
|
|
assert.deepEqual(container.appended_data.html(), "(1, 43)(4, 11)(6, 7)");
|
|
|
|
// We can sort without registering the function, too.
|
|
function sort_by_y(a, b) {
|
|
return a.y - b.y;
|
|
}
|
|
|
|
widget.sort(sort_by_y);
|
|
assert.deepEqual(container.appended_data.html(), "(6, 7)(4, 11)(1, 43)");
|
|
});
|
|
|
|
run_test("clear_event_handlers", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
const sort_container = make_sort_container();
|
|
const filter_element = make_filter_element();
|
|
|
|
// We don't care about actual data for this test.
|
|
const list = [];
|
|
container.html = () => {};
|
|
|
|
const opts = {
|
|
name: "list-we-create-twice",
|
|
parent_container: sort_container,
|
|
modifier: () => {},
|
|
filter: {
|
|
element: filter_element,
|
|
predicate: () => true,
|
|
},
|
|
simplebar_container: scroll_container,
|
|
};
|
|
|
|
// Create it the first time.
|
|
ListWidget.create(container, list, opts);
|
|
assert.equal(sort_container.cleared, false);
|
|
assert.equal(scroll_container.cleared, false);
|
|
assert.equal(filter_element.cleared, false);
|
|
|
|
// The second time we'll clear the old events.
|
|
ListWidget.create(container, list, opts);
|
|
assert.equal(sort_container.cleared, true);
|
|
assert.equal(scroll_container.cleared, true);
|
|
assert.equal(filter_element.cleared, true);
|
|
});
|
|
|
|
run_test("errors", () => {
|
|
// We don't care about actual data for this test.
|
|
const list = ["stub"];
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
|
|
blueslip.expect("error", "Need opts to create widget.");
|
|
ListWidget.create(container, list);
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "simplebar_container is missing.");
|
|
ListWidget.create(container, list, {
|
|
modifier: "hello world",
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "get_item should be a function");
|
|
ListWidget.create(container, list, {
|
|
get_item: "not a function",
|
|
simplebar_container: scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "Filter predicate is not a function.");
|
|
ListWidget.create(container, list, {
|
|
filter: {
|
|
predicate: "wrong type",
|
|
},
|
|
simplebar_container: scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "Filterer and predicate are mutually exclusive.");
|
|
ListWidget.create(container, list, {
|
|
filter: {
|
|
filterer: () => true,
|
|
predicate: () => true,
|
|
},
|
|
simplebar_container: scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
blueslip.expect("error", "Filter filterer is not a function (or missing).");
|
|
ListWidget.create(container, list, {
|
|
filter: {},
|
|
simplebar_container: scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
container.html = () => {};
|
|
blueslip.expect("error", "List item is not a string: 999");
|
|
ListWidget.create(container, list, {
|
|
modifier: () => 999,
|
|
simplebar_container: scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
});
|
|
|
|
run_test("sort helpers", () => {
|
|
/*
|
|
We mostly test our sorting helpers using the
|
|
actual widget, but this test gets us a bit
|
|
more line coverage.
|
|
*/
|
|
const alice2 = {name: "alice", id: 2};
|
|
const alice10 = {name: "alice", id: 10};
|
|
const bob2 = {name: "bob", id: 2};
|
|
const bob10 = {name: "bob", id: 10};
|
|
|
|
const alpha_cmp = ListWidget.alphabetic_sort("name");
|
|
const num_cmp = ListWidget.numeric_sort("id");
|
|
|
|
assert.equal(alpha_cmp(alice2, alice10), 0);
|
|
assert.equal(alpha_cmp(alice2, bob2), -1);
|
|
assert.equal(alpha_cmp(bob2, alice10), 1);
|
|
assert.equal(num_cmp(alice2, bob2), 0);
|
|
assert.equal(num_cmp(alice2, bob10), -1);
|
|
assert.equal(num_cmp(alice10, bob2), 1);
|
|
});
|
|
|
|
run_test("replace_list_data w/filter update", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
container.html = () => {};
|
|
|
|
const list = [1, 2, 3, 4];
|
|
let num_updates = 0;
|
|
|
|
ListWidget.create(container, list, {
|
|
name: "replace-list",
|
|
modifier: (n) => "(" + n.toString() + ")",
|
|
filter: {
|
|
predicate: (n) => n % 2 === 0,
|
|
onupdate: () => {
|
|
num_updates += 1;
|
|
},
|
|
},
|
|
simplebar_container: scroll_container,
|
|
});
|
|
|
|
assert.equal(num_updates, 0);
|
|
|
|
assert.deepEqual(container.appended_data.html(), "(2)(4)");
|
|
|
|
const widget = ListWidget.get("replace-list");
|
|
widget.replace_list_data([5, 6, 7, 8]);
|
|
|
|
assert.equal(num_updates, 1);
|
|
|
|
assert.deepEqual(container.appended_data.html(), "(6)(8)");
|
|
});
|
|
|
|
run_test("opts.get_item", () => {
|
|
const items = {};
|
|
|
|
items[1] = "one";
|
|
items[2] = "two";
|
|
items[3] = "three";
|
|
items[4] = "four";
|
|
|
|
const list = [1, 2, 3, 4];
|
|
|
|
const boring_opts = {
|
|
get_item: (n) => items[n],
|
|
};
|
|
|
|
assert.deepEqual(ListWidget.get_filtered_items("whatever", list, boring_opts), [
|
|
"one",
|
|
"two",
|
|
"three",
|
|
"four",
|
|
]);
|
|
|
|
const predicate = (item, value) => item.startsWith(value);
|
|
|
|
const predicate_opts = {
|
|
get_item: (n) => items[n],
|
|
filter: {
|
|
predicate,
|
|
},
|
|
};
|
|
|
|
assert.deepEqual(ListWidget.get_filtered_items("t", list, predicate_opts), ["two", "three"]);
|
|
|
|
const filterer_opts = {
|
|
get_item: (n) => items[n],
|
|
filter: {
|
|
filterer: (items, value) => items.filter((item) => predicate(item, value)),
|
|
},
|
|
};
|
|
|
|
assert.deepEqual(ListWidget.get_filtered_items("t", list, filterer_opts), ["two", "three"]);
|
|
});
|
|
|
|
run_test("render item", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
const INITIAL_RENDER_COUNT = 80; // Keep this in sync with the actual code.
|
|
container.html = () => {};
|
|
let called = false;
|
|
scroll_container.find = (query) => {
|
|
const expected_queries = [
|
|
`tr[data-item='${INITIAL_RENDER_COUNT}']`,
|
|
`tr[data-item='${INITIAL_RENDER_COUNT - 1}']`,
|
|
];
|
|
const item = INITIAL_RENDER_COUNT - 1;
|
|
const new_html = `<tr data-item=${item}>updated: ${item}</tr>\n`;
|
|
const regex = new RegExp(`\\<tr data-item=${item}\\>.*?<\\/tr\\>`);
|
|
assert.ok(expected_queries.includes(query));
|
|
if (query.includes(`data-item='${INITIAL_RENDER_COUNT}'`)) {
|
|
return undefined; // This item is not rendered, so we find nothing
|
|
}
|
|
return {
|
|
// Return a JQuery stub for the original HTML.
|
|
// We want this to be called when we replace
|
|
// the existing HTML with newly rendered HTML.
|
|
replaceWith: (html) => {
|
|
assert.equal(new_html, html);
|
|
called = true;
|
|
container.appended_data.replace(regex, new_html);
|
|
},
|
|
};
|
|
};
|
|
|
|
const list = [...Array.from({length: 100}).keys()];
|
|
|
|
let text = "initial";
|
|
const get_item = (item) => ({text: `${text}: ${item}`, value: item});
|
|
|
|
const widget = ListWidget.create(container, list, {
|
|
name: "replace-list",
|
|
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
|
|
get_item,
|
|
html_selector: (item) => `tr[data-item='${item}']`,
|
|
simplebar_container: scroll_container,
|
|
});
|
|
const item = INITIAL_RENDER_COUNT - 1;
|
|
|
|
assert.ok(container.appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
|
|
assert.ok(container.appended_data.html().includes("<tr data-item=3>initial: 3</tr>"));
|
|
text = "updated";
|
|
called = false;
|
|
widget.render_item(INITIAL_RENDER_COUNT - 1);
|
|
assert.ok(called);
|
|
assert.ok(container.appended_data.html().includes("<tr data-item=2>initial: 2</tr>"));
|
|
assert.ok(
|
|
container.appended_data.html().includes(`<tr data-item=${item}>updated: ${item}</tr>`),
|
|
);
|
|
|
|
// Item 80 should not be in the rendered list. (0 indexed)
|
|
assert.ok(
|
|
!container.appended_data
|
|
.html()
|
|
.includes(
|
|
`<tr data-item=${INITIAL_RENDER_COUNT}>initial: ${INITIAL_RENDER_COUNT}</tr>`,
|
|
),
|
|
);
|
|
called = false;
|
|
widget.render_item(INITIAL_RENDER_COUNT);
|
|
assert.ok(!called);
|
|
widget.render_item(INITIAL_RENDER_COUNT - 1);
|
|
assert.ok(called);
|
|
|
|
// Tests below this are for the corner cases, where we abort the rerender.
|
|
|
|
blueslip.expect("error", "html_selector should be a function.");
|
|
ListWidget.create(container, list, {
|
|
name: "replace-list",
|
|
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
|
|
get_item,
|
|
html_selector: "hello world",
|
|
simplebar_container: scroll_container,
|
|
});
|
|
blueslip.reset();
|
|
|
|
let get_item_called;
|
|
const widget_2 = ListWidget.create(container, list, {
|
|
name: "replace-list",
|
|
modifier: (item) => `<tr data-item=${item.value}>${item.text}</tr>\n`,
|
|
get_item: (item) => {
|
|
get_item_called = true;
|
|
return item;
|
|
},
|
|
simplebar_container: scroll_container,
|
|
});
|
|
get_item_called = false;
|
|
widget_2.render_item(item);
|
|
// Test that we didn't try to render the item.
|
|
assert.ok(!get_item_called);
|
|
|
|
let rendering_item = false;
|
|
const widget_3 = ListWidget.create(container, list, {
|
|
name: "replace-list",
|
|
modifier: (item) => (rendering_item ? undefined : `${item}\n`),
|
|
get_item,
|
|
html_selector: (item) => `tr[data-item='${item}']`,
|
|
simplebar_container: scroll_container,
|
|
});
|
|
// Once we have initially rendered the widget, change the
|
|
// behavior of the modifier function.
|
|
rendering_item = true;
|
|
blueslip.expect("error", "List item is not a string: undefined");
|
|
widget_3.render_item(item);
|
|
blueslip.reset();
|
|
});
|
|
|
|
run_test("Multiselect dropdown retain_selected_items", () => {
|
|
const container = make_container();
|
|
const scroll_container = make_scroll_container();
|
|
const filter_element = make_filter_element();
|
|
let data_rendered = [];
|
|
|
|
const list = ["one", "two", "three", "four"].map((x) => ({name: x, value: x}));
|
|
const data = ["one"]; // Data initially selected.
|
|
|
|
container.html = () => {};
|
|
container.find = (elem) => DropdownItem(elem);
|
|
|
|
// We essentially create fake Jquery functions
|
|
// whose return value are stored in objects so that
|
|
// they can be later asserted with expected values.
|
|
function DropdownItem(element) {
|
|
const temp = {};
|
|
|
|
function length() {
|
|
if (element) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function find(tag) {
|
|
return ListItem(tag, temp);
|
|
}
|
|
|
|
function addClass(cls) {
|
|
temp.appended_class = cls;
|
|
}
|
|
|
|
temp.element = element;
|
|
return {
|
|
length: length(),
|
|
find,
|
|
addClass,
|
|
};
|
|
}
|
|
|
|
function ListItem(element, temp) {
|
|
function expectOne() {
|
|
data_rendered.push(temp);
|
|
return ListItem(element, temp);
|
|
}
|
|
|
|
function prepend(data) {
|
|
temp.prepended_data = data.html();
|
|
}
|
|
|
|
return {
|
|
expectOne,
|
|
prepend,
|
|
};
|
|
}
|
|
|
|
const widget = ListWidget.create(container, list, {
|
|
name: "replace-list",
|
|
modifier: (item) => `<li data-value="${item.value}">${item.name}</li>\n`,
|
|
multiselect: {
|
|
selected_items: data,
|
|
},
|
|
filter: {
|
|
element: filter_element,
|
|
predicate: () => true,
|
|
},
|
|
simplebar_container: scroll_container,
|
|
});
|
|
|
|
const expected_value = [
|
|
{
|
|
element: 'li[data-value = "one"]',
|
|
appended_class: "checked",
|
|
prepended_data: "<i>",
|
|
},
|
|
];
|
|
|
|
assert.deepEqual(expected_value, data_rendered);
|
|
|
|
// Reset the variable and re execute the `widget.render` method.
|
|
data_rendered = [];
|
|
|
|
// Making sure!
|
|
assert.deepEqual(data_rendered, []);
|
|
|
|
widget.hard_redraw();
|
|
|
|
// Expect the `data_rendered` array to be same again.
|
|
assert.deepEqual(expected_value, data_rendered);
|
|
});
|