zulip/static/js/dropdown_list_widget.js

168 lines
5.5 KiB
JavaScript

import $ from "jquery";
import render_dropdown_list from "../templates/settings/dropdown_list.hbs";
import * as blueslip from "./blueslip";
import * as ListWidget from "./list_widget";
export const DropdownListWidget = function ({
widget_name,
data,
default_text,
render_text = (item_name) => item_name,
null_value = null,
include_current_item = true,
value,
on_update = () => {},
}) {
const container_id = `${widget_name}_widget`;
const value_id = `id_${widget_name}`;
if (value === undefined) {
value = null_value;
blueslip.warn("dropdown-list-widget: Called without a default value; using null value");
}
const render_default_text = (elem) => {
elem.text(default_text);
elem.addClass("text-warning");
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").hide();
};
const render = (value) => {
$(`#${CSS.escape(container_id)} #${CSS.escape(value_id)}`).data("value", value);
const elem = $(`#${CSS.escape(container_id)} #${CSS.escape(widget_name)}_name`);
if (!value || value === null_value) {
render_default_text(elem);
return;
}
// Happy path
const item = data.find((x) => x.value === value.toString());
if (item === undefined) {
render_default_text(elem);
return;
}
const text = render_text(item.name);
elem.text(text);
elem.removeClass("text-warning");
elem.closest(".input-group").find(".dropdown_list_reset_button:enabled").show();
};
const update = (value) => {
render(value);
on_update(value);
};
const register_event_handlers = () => {
$(`#${CSS.escape(container_id)} .dropdown-list-body`).on(
"click keypress",
".list_item",
function (e) {
const setting_elem = $(this).closest(`.${CSS.escape(widget_name)}_setting`);
if (e.type === "keypress") {
if (e.key === "Enter") {
setting_elem.find(".dropdown-menu").dropdown("toggle");
} else {
return;
}
}
const value = $(this).attr("data-value");
update(value);
},
);
$(`#${CSS.escape(container_id)} .dropdown_list_reset_button`).on("click", (e) => {
update(null_value);
e.preventDefault();
});
};
const setup = () => {
// populate the dropdown
const dropdown_list_body = $(
`#${CSS.escape(container_id)} .dropdown-list-body`,
).expectOne();
const search_input = $(`#${CSS.escape(container_id)} .dropdown-search > input[type=text]`);
const dropdown_toggle = $(`#${CSS.escape(container_id)} .dropdown-toggle`);
const get_data = (data) => {
if (include_current_item) {
return data;
}
return data.filter((x) => x.value !== value.toString());
};
ListWidget.create(dropdown_list_body, get_data(data), {
name: `${CSS.escape(widget_name)}_list`,
modifier(item) {
return render_dropdown_list({item});
},
filter: {
element: search_input,
predicate(item, value) {
return item.name.toLowerCase().includes(value);
},
},
simplebar_container: $(`#${CSS.escape(container_id)} .dropdown-list-wrapper`),
});
$(`#${CSS.escape(container_id)} .dropdown-search`).on("click", (e) => {
e.stopPropagation();
});
dropdown_toggle.on("click", () => {
search_input.val("").trigger("input");
});
dropdown_toggle.on("focus", (e) => {
// On opening a Bootstrap Dropdown, the parent element receives focus.
// Here, we want our search input to have focus instead.
e.preventDefault();
// This function gets called twice when focusing the
// dropdown, and only in the second call is the input
// field visible in the DOM; so the following visibility
// check ensures we wait for the second call to focus.
if (dropdown_list_body.is(":visible")) {
search_input.trigger("focus");
}
});
search_input.on("keydown", (e) => {
const {key, keyCode, which} = e;
const navigation_keys = ["ArrowUp", "ArrowDown", "Escape"];
if (!navigation_keys.includes(key)) {
return;
}
e.preventDefault();
e.stopPropagation();
// We pass keyCode instead of key here because the outdated
// bootstrap library we have at static/third/ still uses the
// deprecated keyCode & which properties.
const custom_event = new $.Event("keydown.dropdown.data-api", {keyCode, which});
dropdown_toggle.trigger(custom_event);
});
render(value);
register_event_handlers();
};
const get_value = () => {
let val = $(`#${CSS.escape(container_id)} #${CSS.escape(value_id)}`).data("value");
if (val === null) {
val = "";
}
return val;
};
// Run setup() automatically on initialization.
setup();
return {
render,
value: get_value,
update,
};
};