mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 15:03:34 +00:00
After topic list is updated, only restore focus to it, if it was focused before. This avoids jumping focus from say compose box to topic search unexpectedly when the topic list is updated.
373 lines
9.9 KiB
JavaScript
373 lines
9.9 KiB
JavaScript
import $ from "jquery";
|
|
import _ from "lodash";
|
|
|
|
import render_filter_topics from "../templates/filter_topics.hbs";
|
|
import render_more_topics from "../templates/more_topics.hbs";
|
|
import render_more_topics_spinner from "../templates/more_topics_spinner.hbs";
|
|
import render_topic_list_item from "../templates/topic_list_item.hbs";
|
|
|
|
import * as blueslip from "./blueslip";
|
|
import * as narrow from "./narrow";
|
|
import * as stream_popover from "./stream_popover";
|
|
import * as stream_topic_history from "./stream_topic_history";
|
|
import * as stream_topic_history_util from "./stream_topic_history_util";
|
|
import * as sub_store from "./sub_store";
|
|
import * as topic_list_data from "./topic_list_data";
|
|
import * as ui from "./ui";
|
|
import * as vdom from "./vdom";
|
|
|
|
/*
|
|
Track all active widgets with a Map.
|
|
|
|
(We have at max one for now, but we may
|
|
eventually allow multiple streams to be
|
|
expanded.)
|
|
*/
|
|
|
|
const active_widgets = new Map();
|
|
|
|
// We know whether we're zoomed or not.
|
|
let zoomed = false;
|
|
|
|
export function update() {
|
|
for (const widget of active_widgets.values()) {
|
|
widget.build();
|
|
}
|
|
}
|
|
|
|
export function clear() {
|
|
stream_popover.hide_topic_popover();
|
|
|
|
for (const widget of active_widgets.values()) {
|
|
widget.remove();
|
|
}
|
|
|
|
active_widgets.clear();
|
|
}
|
|
|
|
export function close() {
|
|
zoomed = false;
|
|
clear();
|
|
}
|
|
|
|
export function zoom_out() {
|
|
zoomed = false;
|
|
|
|
const stream_ids = Array.from(active_widgets.keys());
|
|
|
|
if (stream_ids.length !== 1) {
|
|
blueslip.error("Unexpected number of topic lists to zoom out.");
|
|
return;
|
|
}
|
|
|
|
const stream_id = stream_ids[0];
|
|
const widget = active_widgets.get(stream_id);
|
|
const parent_widget = widget.get_parent();
|
|
|
|
rebuild(parent_widget, stream_id);
|
|
}
|
|
|
|
export function keyed_topic_li(convo) {
|
|
const render = () => render_topic_list_item(convo);
|
|
|
|
const eq = (other) => _.isEqual(convo, other.convo);
|
|
|
|
const key = "t:" + convo.topic_name;
|
|
|
|
return {
|
|
key,
|
|
render,
|
|
convo,
|
|
eq,
|
|
};
|
|
}
|
|
|
|
export function more_li(more_topics_unreads) {
|
|
const render = () =>
|
|
render_more_topics({
|
|
more_topics_unreads,
|
|
});
|
|
|
|
const eq = (other) => other.more_items && more_topics_unreads === other.more_topics_unreads;
|
|
|
|
const key = "more";
|
|
|
|
return {
|
|
key,
|
|
more_items: true,
|
|
more_topics_unreads,
|
|
render,
|
|
eq,
|
|
};
|
|
}
|
|
|
|
export function spinner_li() {
|
|
const render = () => render_more_topics_spinner();
|
|
|
|
const eq = (other) => other.spinner;
|
|
|
|
const key = "more";
|
|
|
|
return {
|
|
key,
|
|
spinner: true,
|
|
render,
|
|
eq,
|
|
};
|
|
}
|
|
|
|
function filter_topics_li() {
|
|
const eq = (other) => other.filter_topics;
|
|
|
|
return {
|
|
key: "filter",
|
|
filter_topics: true,
|
|
render: render_filter_topics,
|
|
eq,
|
|
};
|
|
}
|
|
|
|
export class TopicListWidget {
|
|
prior_dom = undefined;
|
|
|
|
constructor(parent_elem, my_stream_id) {
|
|
this.parent_elem = parent_elem;
|
|
this.my_stream_id = my_stream_id;
|
|
this.topic_search_text = "";
|
|
this.topic_search_focused_before_build = true;
|
|
}
|
|
|
|
build_list(spinner) {
|
|
const list_info = topic_list_data.get_list_info(this.my_stream_id, zoomed);
|
|
|
|
const num_possible_topics = list_info.num_possible_topics;
|
|
const more_topics_unreads = list_info.more_topics_unreads;
|
|
|
|
const is_showing_all_possible_topics =
|
|
list_info.items.length === num_possible_topics &&
|
|
stream_topic_history.is_complete_for_stream_id(this.my_stream_id);
|
|
|
|
const attrs = [["class", "topic-list"]];
|
|
|
|
const nodes = list_info.items.map((convo) => keyed_topic_li(convo));
|
|
|
|
if (spinner) {
|
|
nodes.push(spinner_li());
|
|
} else if (!is_showing_all_possible_topics) {
|
|
nodes.push(more_li(more_topics_unreads));
|
|
} else if (zoomed) {
|
|
// In the zoomed topic view, we need to add the input
|
|
// for filtering through list of topics.
|
|
nodes.unshift(filter_topics_li());
|
|
}
|
|
|
|
const dom = vdom.ul({
|
|
attrs,
|
|
keyed_nodes: nodes,
|
|
});
|
|
|
|
return dom;
|
|
}
|
|
|
|
get_parent() {
|
|
return this.parent_elem;
|
|
}
|
|
|
|
get_stream_id() {
|
|
return this.my_stream_id;
|
|
}
|
|
|
|
update_topic_search_text(text) {
|
|
this.topic_search_text = text;
|
|
}
|
|
|
|
update_topic_search_input() {
|
|
const input = this.parent_elem.find("#filter-topic-input");
|
|
if (input.length) {
|
|
// Restore topic search text saved in remove()
|
|
// after the element was rerendered.
|
|
input.val(this.topic_search_text);
|
|
if (this.topic_search_focused_before_build) {
|
|
// Don't focus topic search if it wasn't focused before.
|
|
// This avoids unwanted change of focus.
|
|
input.trigger("focus");
|
|
}
|
|
|
|
// setup display of clear(x) button.
|
|
if (this.topic_search_text.length) {
|
|
$("#clear_search_topic_button").show();
|
|
} else {
|
|
$("#clear_search_topic_button").hide();
|
|
}
|
|
|
|
// setup event handlers.
|
|
const rebuild_list = () => this.build();
|
|
input.on("input", rebuild_list);
|
|
}
|
|
}
|
|
|
|
remove() {
|
|
// If text was present in the topic search filter, we store
|
|
// the input value lazily before removing old elements. This
|
|
// is a workaround for the quirk that the filter input is part
|
|
// of the region that we rerender.
|
|
const input = this.parent_elem.find("#filter-topic-input");
|
|
if (input.length) {
|
|
this.update_topic_search_text(input.val());
|
|
} else {
|
|
// Clear the topic search input when zooming out.
|
|
this.update_topic_search_text("");
|
|
}
|
|
this.parent_elem.find(".topic-list").remove();
|
|
this.prior_dom = undefined;
|
|
}
|
|
|
|
build(spinner) {
|
|
const new_dom = this.build_list(spinner);
|
|
|
|
const replace_content = (html) => {
|
|
this.remove();
|
|
this.parent_elem.append(html);
|
|
this.update_topic_search_input();
|
|
};
|
|
|
|
const find = () => this.parent_elem.find(".topic-list");
|
|
|
|
this.topic_search_focused_before_build = document.activeElement.id === "filter-topic-input";
|
|
vdom.update(replace_content, find, new_dom, this.prior_dom);
|
|
|
|
this.prior_dom = new_dom;
|
|
}
|
|
}
|
|
|
|
export function clear_topic_search(e) {
|
|
e.stopPropagation();
|
|
const input = $("#filter-topic-input");
|
|
if (input.length) {
|
|
input.val("");
|
|
input.trigger("blur");
|
|
|
|
// Since this changes the contents of the search input, we
|
|
// need to rerender the topic list.
|
|
const stream_ids = Array.from(active_widgets.keys());
|
|
|
|
const stream_id = stream_ids[0];
|
|
const widget = active_widgets.get(stream_id);
|
|
const parent_widget = widget.get_parent();
|
|
|
|
rebuild(parent_widget, stream_id);
|
|
}
|
|
}
|
|
|
|
export function active_stream_id() {
|
|
const stream_ids = Array.from(active_widgets.keys());
|
|
|
|
if (stream_ids.length !== 1) {
|
|
return undefined;
|
|
}
|
|
|
|
return stream_ids[0];
|
|
}
|
|
|
|
export function get_stream_li() {
|
|
const widgets = Array.from(active_widgets.values());
|
|
|
|
if (widgets.length !== 1) {
|
|
return undefined;
|
|
}
|
|
|
|
const stream_li = widgets[0].get_parent();
|
|
return stream_li;
|
|
}
|
|
|
|
export function rebuild(stream_li, stream_id) {
|
|
const active_widget = active_widgets.get(stream_id);
|
|
|
|
if (active_widget) {
|
|
active_widget.build();
|
|
return;
|
|
}
|
|
|
|
clear();
|
|
const widget = new TopicListWidget(stream_li, stream_id);
|
|
widget.build();
|
|
|
|
active_widgets.set(stream_id, widget);
|
|
}
|
|
|
|
// For zooming, we only do topic-list stuff here...let stream_list
|
|
// handle hiding/showing the non-narrowed streams
|
|
export function zoom_in() {
|
|
zoomed = true;
|
|
|
|
const stream_id = active_stream_id();
|
|
if (!stream_id) {
|
|
blueslip.error("Cannot find widget for topic history zooming.");
|
|
return;
|
|
}
|
|
|
|
const active_widget = active_widgets.get(stream_id);
|
|
|
|
function on_success() {
|
|
if (!active_widgets.has(stream_id)) {
|
|
blueslip.warn("User re-narrowed before topic history was returned.");
|
|
return;
|
|
}
|
|
|
|
if (!zoomed) {
|
|
blueslip.warn("User zoomed out before topic history was returned.");
|
|
// Note that we could attempt to re-draw the zoomed out topic list
|
|
// here, given that we have more history, but that might be more
|
|
// confusing than helpful to a user who is likely trying to browse
|
|
// other streams.
|
|
return;
|
|
}
|
|
|
|
active_widget.build();
|
|
}
|
|
|
|
ui.get_scroll_element($("#stream-filters-container")).scrollTop(0);
|
|
|
|
const spinner = true;
|
|
active_widget.build(spinner);
|
|
|
|
stream_topic_history_util.get_server_history(stream_id, on_success);
|
|
}
|
|
|
|
export function get_topic_search_term() {
|
|
const filter = $("#filter-topic-input");
|
|
if (filter.val() === undefined) {
|
|
return "";
|
|
}
|
|
return filter.val().trim();
|
|
}
|
|
|
|
export function initialize() {
|
|
$("#stream_filters").on("click", ".topic-box", (e) => {
|
|
if (e.metaKey || e.ctrlKey) {
|
|
return;
|
|
}
|
|
if ($(e.target).closest(".show-more-topics").length > 0) {
|
|
return;
|
|
}
|
|
|
|
// In a more componentized world, we would delegate some
|
|
// of this stuff back up to our parents.
|
|
|
|
const stream_row = $(e.target).parents(".narrow-filter");
|
|
const stream_id = Number.parseInt(stream_row.attr("data-stream-id"), 10);
|
|
const sub = sub_store.get(stream_id);
|
|
const topic = $(e.target).parents("li").attr("data-topic-name");
|
|
|
|
narrow.activate(
|
|
[
|
|
{operator: "stream", operand: sub.name},
|
|
{operator: "topic", operand: topic},
|
|
],
|
|
{trigger: "sidebar"},
|
|
);
|
|
|
|
e.preventDefault();
|
|
});
|
|
}
|