2021-03-11 05:43:45 +01:00
|
|
|
import $ from "jquery";
|
2023-04-28 08:55:36 +02:00
|
|
|
import assert from "minimalistic-assert";
|
2021-03-11 05:43:45 +01:00
|
|
|
|
2021-03-16 23:38:59 +01:00
|
|
|
import * as blueslip from "./blueslip";
|
2023-04-25 18:01:02 +02:00
|
|
|
import * as scroll_util from "./scroll_util";
|
2021-02-28 21:33:10 +01:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
type SortingFunction<T> = (a: T, b: T) => number;
|
|
|
|
type GenericSortingFunction<T = Record<string, unknown>> = (prop: string) => SortingFunction<T>;
|
|
|
|
|
|
|
|
type ListWidgetMeta<Key = unknown, Item = Key> = {
|
|
|
|
sorting_function: SortingFunction<Item> | null;
|
|
|
|
sorting_functions: Map<string, SortingFunction<Item>>;
|
|
|
|
filter_value: string;
|
|
|
|
offset: number;
|
|
|
|
list: Key[];
|
|
|
|
filtered_list: Item[];
|
|
|
|
reverse_mode: boolean;
|
|
|
|
$scroll_container: JQuery;
|
|
|
|
};
|
|
|
|
|
|
|
|
// This type ensures the mutually exclusive nature of the predicate and filterer options.
|
|
|
|
type ListWidgetFilterOpts<Item = unknown> = {
|
2023-09-08 22:52:44 +02:00
|
|
|
$element?: JQuery<HTMLInputElement>;
|
2023-04-28 08:55:36 +02:00
|
|
|
onupdate?: () => void;
|
|
|
|
} & (
|
|
|
|
| {
|
|
|
|
predicate: (item: Item, value: string) => boolean;
|
|
|
|
filterer?: never;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
predicate?: never;
|
|
|
|
filterer: (list: Item[], value: string) => Item[];
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
type ListWidgetOpts<Key = unknown, Item = Key> = {
|
|
|
|
name?: string;
|
|
|
|
get_item: (key: Key) => Item;
|
2023-08-15 01:00:29 +02:00
|
|
|
modifier: (item: Item, filter_value: string) => string;
|
2023-04-28 08:55:36 +02:00
|
|
|
init_sort?: string | SortingFunction<Item>;
|
|
|
|
initially_descending_sort?: boolean;
|
|
|
|
html_selector?: (item: Item) => JQuery;
|
|
|
|
callback_after_render?: () => void;
|
|
|
|
post_scroll__pre_render_callback?: () => void;
|
|
|
|
get_min_load_count?: (rendered_count: number, load_count: number) => number;
|
|
|
|
is_scroll_position_for_render?: (scroll_container: HTMLElement) => boolean;
|
|
|
|
filter?: ListWidgetFilterOpts<Item>;
|
|
|
|
multiselect?: {
|
|
|
|
selected_items: Key[];
|
|
|
|
};
|
|
|
|
sort_fields?: Record<string, SortingFunction<Item>>;
|
|
|
|
$simplebar_container: JQuery;
|
|
|
|
$parent_container?: JQuery;
|
|
|
|
};
|
|
|
|
|
|
|
|
type ListWidget<Key = unknown, Item = Key> = {
|
|
|
|
get_current_list(): Item[];
|
|
|
|
filter_and_sort(): void;
|
|
|
|
retain_selected_items(): void;
|
|
|
|
render(how_many?: number): void;
|
|
|
|
render_item(item: Item): void;
|
|
|
|
clear(): void;
|
|
|
|
set_filter_value(value: string): void;
|
|
|
|
set_reverse_mode(reverse_mode: boolean): void;
|
|
|
|
set_sorting_function(sorting_function: string | SortingFunction<Item>): void;
|
|
|
|
set_up_event_handlers(): void;
|
|
|
|
clear_event_handlers(): void;
|
|
|
|
increase_rendered_offset(): void;
|
|
|
|
reduce_rendered_offset(): void;
|
|
|
|
remove_rendered_row(row: JQuery): void;
|
|
|
|
clean_redraw(): void;
|
|
|
|
hard_redraw(): void;
|
|
|
|
insert_rendered_row(item: Item, get_insert_index: (list: Item[], item: Item) => number): void;
|
|
|
|
sort(sorting_function: string, prop?: string): void;
|
|
|
|
replace_list_data(list: Key[]): void;
|
|
|
|
};
|
|
|
|
|
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,
|
2023-04-28 08:55:36 +02:00
|
|
|
instances: new Map<string, ListWidget>(),
|
2019-10-25 09:15:16 +02:00
|
|
|
};
|
|
|
|
|
2020-05-28 19:43:59 +02:00
|
|
|
// ----------------------------------------------------
|
2021-01-29 10:27:56 +01:00
|
|
|
// This function describes (programmatically) how to use the ListWidget.
|
2020-05-28 19:43:59 +02:00
|
|
|
// ----------------------------------------------------
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export function get_filtered_items<Key, Item>(
|
|
|
|
value: string,
|
|
|
|
list: Key[],
|
|
|
|
opts: ListWidgetOpts<Key, Item>,
|
|
|
|
): Item[] {
|
2019-12-30 16:13:42 +01:00
|
|
|
/*
|
|
|
|
This is used by the main object (see `create`),
|
|
|
|
but we split it out to make it a bit easier
|
|
|
|
to test.
|
|
|
|
*/
|
2020-05-10 12:45:16 +02:00
|
|
|
const get_item = opts.get_item;
|
|
|
|
|
2020-04-13 16:13:06 +02:00
|
|
|
if (!opts.filter) {
|
2023-05-01 13:44:40 +02:00
|
|
|
return list.map((key) => get_item(key));
|
2020-04-13 16:13:06 +02:00
|
|
|
}
|
|
|
|
|
2020-01-13 17:45:53 +01:00
|
|
|
if (opts.filter.filterer) {
|
2023-05-01 13:44:40 +02:00
|
|
|
return opts.filter.filterer(
|
|
|
|
list.map((key) => get_item(key)),
|
|
|
|
value,
|
|
|
|
);
|
2020-01-13 17:45:53 +01:00
|
|
|
}
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
const predicate = (item: Item): boolean => opts.filter!.predicate!(item, value);
|
2020-05-10 12:45:16 +02:00
|
|
|
|
2023-05-01 13:44:40 +02:00
|
|
|
const result = [];
|
2019-12-30 16:13:42 +01:00
|
|
|
|
2023-05-01 13:44:40 +02:00
|
|
|
for (const key of list) {
|
|
|
|
const item = get_item(key);
|
|
|
|
if (predicate(item)) {
|
|
|
|
result.push(item);
|
2020-05-10 12:45:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-01 13:44:40 +02:00
|
|
|
return result;
|
2021-02-28 00:57:45 +01:00
|
|
|
}
|
2019-12-30 16:13:42 +01:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export const alphabetic_sort: GenericSortingFunction = (prop) =>
|
|
|
|
function (a, b) {
|
2020-07-15 00:34:28 +02:00
|
|
|
// The conversion to uppercase helps make the sorting case insensitive.
|
2023-04-28 08:55:36 +02:00
|
|
|
const str1 = (a[prop] as string).toUpperCase();
|
|
|
|
const str2 = (b[prop] as string).toUpperCase();
|
2020-04-24 19:49:18 +02:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
if (str1 === str2) {
|
|
|
|
return 0;
|
|
|
|
} else if (str1 > str2) {
|
|
|
|
return 1;
|
|
|
|
}
|
2020-04-24 19:49:18 +02:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
return -1;
|
|
|
|
};
|
2020-04-24 19:49:18 +02:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export const numeric_sort: GenericSortingFunction = (prop) =>
|
|
|
|
function (a, b) {
|
|
|
|
const a_prop = Number.parseFloat(a[prop] as string);
|
|
|
|
const b_prop = Number.parseFloat(b[prop] as string);
|
|
|
|
|
|
|
|
if (a_prop > b_prop) {
|
2020-07-15 00:34:28 +02:00
|
|
|
return 1;
|
2023-04-28 08:55:36 +02:00
|
|
|
} else if (a_prop === b_prop) {
|
2020-07-15 00:34:28 +02:00
|
|
|
return 0;
|
|
|
|
}
|
2020-04-24 19:49:18 +02:00
|
|
|
|
2020-07-15 00:34:28 +02:00
|
|
|
return -1;
|
|
|
|
};
|
2020-04-24 19:49:18 +02:00
|
|
|
|
2023-05-03 07:06:19 +02:00
|
|
|
const generic_sorts = {
|
|
|
|
alphabetic: alphabetic_sort,
|
|
|
|
numeric: numeric_sort,
|
|
|
|
};
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export function generic_sort_functions<T extends Record<string, unknown>>(
|
|
|
|
generic_func: keyof typeof generic_sorts,
|
|
|
|
props: string[],
|
|
|
|
): Record<string, SortingFunction<T>> {
|
|
|
|
const sorting_functions: Record<string, SortingFunction<T>> = {};
|
2023-05-03 07:06:19 +02:00
|
|
|
for (const prop of props) {
|
|
|
|
const key = `${prop}_${generic_func}`;
|
|
|
|
sorting_functions[key] = generic_sorts[generic_func](prop);
|
|
|
|
}
|
|
|
|
|
|
|
|
return sorting_functions;
|
|
|
|
}
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
function is_scroll_position_for_render(scroll_container: HTMLElement): boolean {
|
2021-05-06 19:06:42 +02:00
|
|
|
return (
|
|
|
|
scroll_container.scrollHeight -
|
|
|
|
(scroll_container.scrollTop + scroll_container.clientHeight) <
|
|
|
|
10
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// @params
|
2022-01-25 11:36:19 +01:00
|
|
|
// $container: jQuery object to append to.
|
2019-10-25 09:15:16 +02:00
|
|
|
// list: The list of items to progressively append.
|
|
|
|
// opts: An object of random preferences.
|
2023-04-28 08:55:36 +02:00
|
|
|
export function create<Key = unknown, Item = Key>(
|
|
|
|
$container: JQuery,
|
|
|
|
list: Key[],
|
|
|
|
opts: ListWidgetOpts<Key, Item>,
|
|
|
|
): ListWidget<Key, Item> | undefined {
|
2020-02-12 08:07:21 +01:00
|
|
|
if (opts.name && DEFAULTS.instances.get(opts.name)) {
|
2020-04-11 16:23:29 +02:00
|
|
|
// Clear event handlers for prior widget.
|
2023-04-28 08:55:36 +02:00
|
|
|
const old_widget = DEFAULTS.instances.get(opts.name)!;
|
2020-04-11 16:23:29 +02:00
|
|
|
old_widget.clear_event_handlers();
|
2019-10-25 09:15:16 +02:00
|
|
|
}
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
const meta: ListWidgetMeta<Key, Item> = {
|
2019-10-25 09:15:16 +02:00
|
|
|
sorting_function: null,
|
2020-02-12 08:08:25 +01:00
|
|
|
sorting_functions: new Map(),
|
2019-10-25 09:15:16 +02:00
|
|
|
offset: 0,
|
2020-07-20 22:18:43 +02:00
|
|
|
list,
|
2023-04-28 08:55:36 +02:00
|
|
|
filtered_list: [],
|
2020-04-13 16:13:06 +02:00
|
|
|
reverse_mode: false,
|
2020-07-15 01:29:15 +02:00
|
|
|
filter_value: "",
|
2023-07-14 13:22:15 +02:00
|
|
|
$scroll_container: scroll_util.get_scroll_element(opts.$simplebar_container),
|
2017-03-16 21:00:56 +01:00
|
|
|
};
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
const widget: ListWidget<Key, Item> = {
|
2023-04-28 06:37:58 +02:00
|
|
|
get_current_list() {
|
|
|
|
return meta.filtered_list;
|
|
|
|
},
|
2021-04-22 23:40:09 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
filter_and_sort() {
|
|
|
|
meta.filtered_list = get_filtered_items(meta.filter_value, meta.list, opts);
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (meta.sorting_function) {
|
|
|
|
meta.filtered_list.sort(meta.sorting_function);
|
|
|
|
}
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (meta.reverse_mode) {
|
|
|
|
meta.filtered_list.reverse();
|
|
|
|
}
|
|
|
|
},
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// Used in case of Multiselect DropdownListWidget to retain
|
|
|
|
// previously checked items even after widget redraws.
|
|
|
|
retain_selected_items() {
|
|
|
|
const items = opts.multiselect;
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
if (items?.selected_items) {
|
2023-04-28 06:37:58 +02:00
|
|
|
const data = items.selected_items;
|
|
|
|
for (const value of data) {
|
2023-04-28 08:55:36 +02:00
|
|
|
const $list_item = $container.find(`li[data-value = "${value as string}"]`);
|
2023-04-28 06:37:58 +02:00
|
|
|
if ($list_item.length) {
|
|
|
|
const $link_elem = $list_item.find("a").expectOne();
|
|
|
|
$list_item.addClass("checked");
|
|
|
|
$link_elem.prepend($("<i>").addClass(["fa", "fa-check"]));
|
|
|
|
}
|
2021-07-27 14:35:58 +02:00
|
|
|
}
|
|
|
|
}
|
2023-04-28 06:37:58 +02:00
|
|
|
},
|
2021-07-27 14:35:58 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// Reads the provided list (in the scope directly above)
|
|
|
|
// and renders the next block of messages automatically
|
|
|
|
// into the specified container.
|
|
|
|
render(how_many) {
|
2023-04-28 08:55:36 +02:00
|
|
|
let load_count = how_many ?? DEFAULTS.LOAD_COUNT;
|
2023-04-28 06:37:58 +02:00
|
|
|
if (opts.get_min_load_count) {
|
|
|
|
load_count = opts.get_min_load_count(meta.offset, load_count);
|
|
|
|
}
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// 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
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
const slice = meta.filtered_list.slice(meta.offset, meta.offset + load_count);
|
2020-04-11 13:52:38 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
let html = "";
|
|
|
|
for (const item of slice) {
|
2023-08-15 01:00:29 +02:00
|
|
|
const s = opts.modifier(item, meta.filter_value);
|
2019-10-25 09:15:16 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (typeof s !== "string") {
|
|
|
|
blueslip.error("List item is not a string", {item: s});
|
|
|
|
continue;
|
|
|
|
}
|
2020-01-15 15:05:44 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// append the HTML or nothing if corrupt (null, undef, etc.).
|
|
|
|
if (s) {
|
|
|
|
html += s;
|
|
|
|
}
|
2020-04-14 23:43:14 +02:00
|
|
|
}
|
2019-10-25 09:15:16 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
$container.append($(html));
|
|
|
|
meta.offset += load_count;
|
2020-09-23 09:37:29 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (opts.multiselect) {
|
|
|
|
widget.retain_selected_items();
|
|
|
|
}
|
2021-07-27 14:35:58 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (opts.callback_after_render) {
|
|
|
|
opts.callback_after_render();
|
|
|
|
}
|
|
|
|
},
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
render_item(item) {
|
|
|
|
if (!opts.html_selector) {
|
|
|
|
// We don't have any way to find the existing item.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const $html_item = meta.$scroll_container.find(opts.html_selector(item));
|
2023-07-07 12:03:25 +02:00
|
|
|
if ($html_item.length === 0) {
|
2023-04-28 06:37:58 +02:00
|
|
|
// We don't have the item in the current scroll container; it'll be
|
|
|
|
// rendered with updated data when it is scrolled to.
|
|
|
|
return;
|
|
|
|
}
|
2020-05-28 19:58:51 +02:00
|
|
|
|
2023-08-15 01:00:29 +02:00
|
|
|
const html = opts.modifier(item, meta.filter_value);
|
2023-04-28 06:37:58 +02:00
|
|
|
if (typeof html !== "string") {
|
|
|
|
blueslip.error("List item is not a string", {item: html});
|
|
|
|
return;
|
|
|
|
}
|
2020-05-28 19:58:51 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// At this point, we have asserted we have all the information to replace
|
|
|
|
// the html now.
|
|
|
|
$html_item.replaceWith(html);
|
|
|
|
},
|
2020-05-28 19:58:51 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
clear() {
|
|
|
|
$container.empty();
|
|
|
|
meta.offset = 0;
|
|
|
|
},
|
2020-04-11 13:52:38 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
set_filter_value(filter_value) {
|
|
|
|
meta.filter_value = filter_value;
|
|
|
|
},
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
set_reverse_mode(reverse_mode) {
|
|
|
|
meta.reverse_mode = reverse_mode;
|
|
|
|
},
|
2020-04-11 13:52:38 +02:00
|
|
|
|
2023-05-03 07:06:19 +02:00
|
|
|
// the sorting function is either the function or a string which will be a key
|
|
|
|
// for the sorting_functions map to get the function. In case of generic sort
|
|
|
|
// functions like numeric and alphabetic, we pass the string in the given format -
|
|
|
|
// "{property}_{numeric|alphabetic}" - e.g. "email_alphabetic" or "age_numeric".
|
|
|
|
set_sorting_function(sorting_function) {
|
2023-04-28 06:37:58 +02:00
|
|
|
if (typeof sorting_function === "function") {
|
|
|
|
meta.sorting_function = sorting_function;
|
|
|
|
} else if (typeof sorting_function === "string") {
|
2023-05-03 07:06:19 +02:00
|
|
|
if (!meta.sorting_functions.has(sorting_function)) {
|
|
|
|
blueslip.error("Sorting function not found: " + sorting_function);
|
|
|
|
return;
|
2023-04-28 06:37:58 +02:00
|
|
|
}
|
2023-05-03 07:06:19 +02:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
meta.sorting_function = meta.sorting_functions.get(sorting_function)!;
|
2019-10-25 09:15:16 +02:00
|
|
|
}
|
2023-04-28 06:37:58 +02:00
|
|
|
},
|
2017-05-05 06:20:38 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
set_up_event_handlers() {
|
|
|
|
// 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.
|
|
|
|
meta.$scroll_container.on("scroll.list_widget_container", function () {
|
|
|
|
if (opts.post_scroll__pre_render_callback) {
|
|
|
|
opts.post_scroll__pre_render_callback();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (opts.is_scroll_position_for_render === undefined) {
|
|
|
|
opts.is_scroll_position_for_render = is_scroll_position_for_render;
|
|
|
|
}
|
|
|
|
|
|
|
|
const should_render = opts.is_scroll_position_for_render(this);
|
|
|
|
if (should_render) {
|
|
|
|
widget.render();
|
|
|
|
}
|
|
|
|
});
|
2021-05-06 19:06:42 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (opts.$parent_container) {
|
|
|
|
opts.$parent_container.on("click.list_widget_sort", "[data-sort]", function () {
|
|
|
|
handle_sort($(this), widget);
|
|
|
|
});
|
2021-05-06 19:06:42 +02:00
|
|
|
}
|
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
opts.filter?.$element?.on("input.list_widget_filter", function () {
|
2023-09-08 22:52:44 +02:00
|
|
|
const value = this.value.toLocaleLowerCase();
|
2023-04-28 08:55:36 +02:00
|
|
|
widget.set_filter_value(value);
|
|
|
|
widget.hard_redraw();
|
|
|
|
});
|
2023-04-28 06:37:58 +02:00
|
|
|
},
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
clear_event_handlers() {
|
|
|
|
meta.$scroll_container.off("scroll.list_widget_container");
|
2020-04-11 13:52:38 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
if (opts.$parent_container) {
|
|
|
|
opts.$parent_container.off("click.list_widget_sort", "[data-sort]");
|
|
|
|
}
|
2020-04-11 16:23:29 +02:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
opts.filter?.$element?.off("input.list_widget_filter");
|
2023-04-28 06:37:58 +02:00
|
|
|
},
|
2020-04-13 11:55:25 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
increase_rendered_offset() {
|
|
|
|
meta.offset = Math.min(meta.offset + 1, meta.filtered_list.length);
|
|
|
|
},
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
reduce_rendered_offset() {
|
|
|
|
meta.offset = Math.max(meta.offset - 1, 0);
|
|
|
|
},
|
2022-11-08 10:58:41 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
remove_rendered_row(rendered_row) {
|
|
|
|
rendered_row.remove();
|
|
|
|
// We removed a rendered row, so we need to reduce one offset.
|
|
|
|
widget.reduce_rendered_offset();
|
|
|
|
},
|
2022-11-08 10:58:41 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
clean_redraw() {
|
|
|
|
widget.filter_and_sort();
|
|
|
|
widget.clear();
|
|
|
|
widget.render(DEFAULTS.INITIAL_RENDER_COUNT);
|
|
|
|
},
|
2022-11-08 10:58:41 +01:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
hard_redraw() {
|
2022-11-08 10:58:41 +01:00
|
|
|
widget.clean_redraw();
|
2023-04-28 08:55:36 +02:00
|
|
|
if (opts.filter?.onupdate) {
|
2023-04-28 06:37:58 +02:00
|
|
|
opts.filter.onupdate();
|
2022-11-08 10:58:41 +01:00
|
|
|
}
|
2023-04-28 06:37:58 +02:00
|
|
|
},
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 09:40:02 +02:00
|
|
|
insert_rendered_row(item, get_insert_index) {
|
2023-04-28 06:37:58 +02:00
|
|
|
// NOTE: Caller should call `filter_and_sort` before calling this function
|
|
|
|
// so that `meta.filtered_list` already has the `item`.
|
|
|
|
if (meta.filtered_list.length <= 2) {
|
|
|
|
// Avoids edge cases for us and could be faster too.
|
|
|
|
widget.clean_redraw();
|
|
|
|
return;
|
|
|
|
}
|
2023-04-28 08:55:36 +02:00
|
|
|
|
|
|
|
assert(
|
|
|
|
opts.filter?.predicate,
|
|
|
|
"filter.predicate should be defined for insert_rendered_row",
|
|
|
|
);
|
|
|
|
if (!opts.filter.predicate(item, meta.filter_value)) {
|
2023-04-28 06:37:58 +02:00
|
|
|
return;
|
|
|
|
}
|
2023-04-28 08:55:36 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// We need to insert the row for it to be displayed at the
|
|
|
|
// correct position. filtered_list must contain the new item
|
|
|
|
// since we know it is not hidden from the above check.
|
2023-04-28 09:40:02 +02:00
|
|
|
const insert_index = get_insert_index(meta.filtered_list, item);
|
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
// Rows greater than `offset` are not rendered in the DOM by list_widget;
|
|
|
|
// for those, there's nothing to update.
|
2023-04-28 09:40:02 +02:00
|
|
|
if (insert_index <= meta.offset) {
|
2023-05-22 13:24:06 +02:00
|
|
|
if (!opts.html_selector) {
|
2023-04-28 06:37:58 +02:00
|
|
|
blueslip.error(
|
|
|
|
"Please specify modifier and html_selector when creating the widget.",
|
|
|
|
);
|
|
|
|
}
|
2023-08-15 01:00:29 +02:00
|
|
|
const rendered_row = opts.modifier(item, meta.filter_value);
|
2023-04-28 09:40:02 +02:00
|
|
|
if (insert_index === meta.filtered_list.length - 1) {
|
2023-04-28 08:55:36 +02:00
|
|
|
const $target_row = opts.html_selector!(meta.filtered_list[insert_index - 1]);
|
2023-04-28 06:37:58 +02:00
|
|
|
$target_row.after(rendered_row);
|
|
|
|
} else {
|
2023-04-28 08:55:36 +02:00
|
|
|
const $target_row = opts.html_selector!(meta.filtered_list[insert_index + 1]);
|
2023-04-28 06:37:58 +02:00
|
|
|
$target_row.before(rendered_row);
|
|
|
|
}
|
|
|
|
widget.increase_rendered_offset();
|
|
|
|
}
|
|
|
|
},
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
sort(sorting_function, prop) {
|
2023-05-03 07:06:19 +02:00
|
|
|
const key = prop ? `${prop}_${sorting_function}` : sorting_function;
|
|
|
|
widget.set_sorting_function(key);
|
2023-04-28 06:37:58 +02:00
|
|
|
widget.hard_redraw();
|
|
|
|
},
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-04-28 06:37:58 +02:00
|
|
|
replace_list_data(list) {
|
|
|
|
/*
|
|
|
|
We mostly use this widget for lists where you are
|
|
|
|
not adding or removing rows, so when you do modify
|
|
|
|
the list, we have a brute force solution.
|
|
|
|
*/
|
|
|
|
meta.list = list;
|
|
|
|
widget.hard_redraw();
|
|
|
|
},
|
2020-04-15 01:29:34 +02:00
|
|
|
};
|
|
|
|
|
2020-04-11 14:07:05 +02:00
|
|
|
widget.set_up_event_handlers();
|
|
|
|
|
2020-04-11 16:23:29 +02:00
|
|
|
if (opts.sort_fields) {
|
|
|
|
for (const [name, sorting_function] of Object.entries(opts.sort_fields)) {
|
|
|
|
meta.sorting_functions.set(name, sorting_function);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (opts.init_sort) {
|
2023-05-03 07:06:19 +02:00
|
|
|
widget.set_sorting_function(opts.init_sort);
|
2020-04-11 16:23:29 +02:00
|
|
|
}
|
|
|
|
|
2023-02-24 00:56:50 +01:00
|
|
|
if (opts.initially_descending_sort) {
|
|
|
|
widget.set_reverse_mode(true);
|
|
|
|
opts.$simplebar_container.find(".active").addClass("descend");
|
|
|
|
}
|
|
|
|
|
2020-04-11 16:23:29 +02:00
|
|
|
widget.clean_redraw();
|
|
|
|
|
2019-10-25 09:15:16 +02:00
|
|
|
// Save the instance for potential future retrieval if a name is provided.
|
|
|
|
if (opts.name) {
|
2020-04-11 13:44:34 +02:00
|
|
|
DEFAULTS.instances.set(opts.name, widget);
|
2019-10-25 09:15:16 +02:00
|
|
|
}
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2020-04-11 13:44:34 +02:00
|
|
|
return widget;
|
2021-02-28 00:57:45 +01:00
|
|
|
}
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export function get(name: string): ListWidget | false {
|
|
|
|
return DEFAULTS.instances.get(name) ?? false;
|
2021-02-28 00:57:45 +01:00
|
|
|
}
|
2017-03-16 21:00:56 +01:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export function handle_sort($th: JQuery, list: ListWidget): void {
|
2019-10-25 09:15:16 +02:00
|
|
|
/*
|
2018-06-21 06:58:19 +02:00
|
|
|
one would specify sort parameters like this:
|
|
|
|
- name => sort alphabetic.
|
|
|
|
- age => sort numeric.
|
2020-04-15 12:22:23 +02:00
|
|
|
- status => look up `status` in sort_fields
|
|
|
|
to find custom sort function
|
|
|
|
|
|
|
|
<thead>
|
|
|
|
<th data-sort="alphabetic" data-sort-prop="name"></th>
|
|
|
|
<th data-sort="numeric" data-sort-prop="age"></th>
|
|
|
|
<th data-sort="status"></th>
|
|
|
|
</thead>
|
2018-06-21 06:58:19 +02:00
|
|
|
*/
|
2023-04-28 08:55:36 +02:00
|
|
|
const sort_type: string = $th.data("sort");
|
|
|
|
const prop_name: string = $th.data("sort-prop");
|
2019-10-25 09:15:16 +02:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
if ($th.hasClass("active")) {
|
|
|
|
if (!$th.hasClass("descend")) {
|
|
|
|
$th.addClass("descend");
|
2019-10-25 09:15:16 +02:00
|
|
|
} else {
|
2022-01-25 11:36:19 +01:00
|
|
|
$th.removeClass("descend");
|
2017-09-28 23:51:34 +02:00
|
|
|
}
|
2020-04-13 16:13:06 +02:00
|
|
|
} else {
|
2022-01-25 11:36:19 +01:00
|
|
|
$th.siblings(".active").removeClass("active");
|
|
|
|
$th.addClass("active");
|
2019-10-25 09:15:16 +02:00
|
|
|
}
|
2017-10-22 19:58:08 +02:00
|
|
|
|
2022-01-25 11:36:19 +01:00
|
|
|
list.set_reverse_mode($th.hasClass("descend"));
|
2020-04-13 16:13:06 +02:00
|
|
|
|
2023-05-03 07:06:19 +02:00
|
|
|
// if `prop_name` is defined, it will trigger the generic sort functions,
|
2019-10-25 09:15:16 +02:00
|
|
|
// and not if it is undefined.
|
|
|
|
list.sort(sort_type, prop_name);
|
2021-02-28 00:57:45 +01:00
|
|
|
}
|
2023-05-01 13:44:40 +02:00
|
|
|
|
2023-04-28 08:55:36 +02:00
|
|
|
export const default_get_item = <T = unknown>(item: T): T => item;
|