/* Module for popovers that have been ported to the modern TippyJS/Popper popover library from the legacy Bootstrap popovers system in popovers.js. */ import $ from "jquery"; import tippy, {delegate} from "tippy.js"; import render_compose_control_buttons_popover from "../templates/compose_control_buttons_popover.hbs"; import render_compose_select_enter_behaviour_popover from "../templates/compose_select_enter_behaviour_popover.hbs"; import render_left_sidebar_stream_setting_popover from "../templates/left_sidebar_stream_setting_popover.hbs"; import render_mobile_message_buttons_popover_content from "../templates/mobile_message_buttons_popover_content.hbs"; import * as channel from "./channel"; import * as common from "./common"; import * as compose_actions from "./compose_actions"; import * as giphy from "./giphy"; import * as narrow_state from "./narrow_state"; import * as popovers from "./popovers"; import * as settings_data from "./settings_data"; import {parse_html} from "./ui_util"; import {user_settings} from "./user_settings"; let left_sidebar_stream_setting_popover_displayed = false; let compose_mobile_button_popover_displayed = false; export let compose_enter_sends_popover_displayed = false; let compose_control_buttons_popover_instance; export function get_compose_control_buttons_popover() { return compose_control_buttons_popover_instance; } const default_popover_props = { delay: 0, appendTo: () => document.body, trigger: "click", interactive: true, hideOnClick: true, /* The light-border TippyJS theme is a bit of a misnomer; it is a popover styling similar to Bootstrap. We've also customized its CSS to support Zulip's dark theme. */ theme: "light-border", touch: true, }; export function any_active() { return ( left_sidebar_stream_setting_popover_displayed || compose_mobile_button_popover_displayed || compose_control_buttons_popover_instance || compose_enter_sends_popover_displayed ); } function on_show_prep(instance) { $(instance.popper).on("click", (e) => { // Popover is not hidden on click inside it unless the click handler for the // element explicitly hides the popover when handling the event. // `stopPropagation` is required here to avoid global click handlers from // being triggered. e.stopPropagation(); }); $(instance.popper).one("click", ".navigate_and_close_popover", (e) => { // Handler for links inside popover which don't need a special click handler. e.stopPropagation(); instance.hide(); }); popovers.hide_all_except_sidebars(instance); } function tippy_no_propagation(target, popover_props) { // For some elements, such as the click target to open the message // actions menu, we want to avoid propagating the click event to // parent elements. Tippy's built-in `delegate` method does not // have an option to do stopPropagation, so we use this method to // open the Tippy popovers associated with such elements. // // A click on the click target will close the menu; for this to // work correctly without leaking, all callers need call // `instance.destroy()` inside their `onHidden` handler. // // TODO: Should we instead we wrap the caller's `onHidden` hook, // if any, to add `instance.destroy()`? $("body").on("click", target, (e) => { e.preventDefault(); e.stopPropagation(); const instance = e.currentTarget._tippy; if (instance) { instance.hide(); return; } tippy(e.currentTarget, { ...default_popover_props, showOnCreate: true, ...popover_props, }); }); } export function initialize() { tippy_no_propagation("#streams_inline_icon", { onShow(instance) { const can_create_streams = settings_data.user_can_create_private_streams() || settings_data.user_can_create_public_streams() || settings_data.user_can_create_web_public_streams(); on_show_prep(instance); if (!can_create_streams) { // If the user can't create streams, we directly // navigate them to the Manage streams subscribe UI. window.location.assign("#streams/all"); // Returning false from an onShow handler cancels the show. return false; } instance.setContent(parse_html(render_left_sidebar_stream_setting_popover())); left_sidebar_stream_setting_popover_displayed = true; return true; }, onHidden(instance) { instance.destroy(); left_sidebar_stream_setting_popover_displayed = false; }, }); // compose box buttons popover shown on mobile widths. // We want this click event to propagate and hide other popovers // that could possibly obstruct user from using this popover. delegate("body", { ...default_popover_props, target: ".compose_mobile_button", placement: "top", onShow(instance) { on_show_prep(instance); instance.setContent( parse_html( render_mobile_message_buttons_popover_content({ is_in_private_narrow: narrow_state.narrowed_to_pms(), }), ), ); compose_mobile_button_popover_displayed = true; }, onMount(instance) { const $popper = $(instance.popper); $popper.one("click", ".compose_mobile_stream_button", (e) => { compose_actions.start("stream", {trigger: "new topic button"}); e.stopPropagation(); instance.hide(); }); $popper.one("click", ".compose_mobile_private_button", (e) => { compose_actions.start("private"); e.stopPropagation(); instance.hide(); }); }, onHidden(instance) { // Destroy instance so that event handlers // are destroyed too. instance.destroy(); compose_mobile_button_popover_displayed = false; }, }); // Click event handlers for it are handled in `compose_ui` and // we don't want to close this popover on click inside it but // only if user clicked outside it. tippy_no_propagation(".compose_control_menu_wrapper", { placement: "top", onShow(instance) { instance.setContent( parse_html( render_compose_control_buttons_popover({ giphy_enabled: giphy.is_giphy_enabled(), }), ), ); compose_control_buttons_popover_instance = instance; popovers.hide_all_except_sidebars(instance); }, onHidden(instance) { instance.destroy(); compose_control_buttons_popover_instance = undefined; }, }); tippy_no_propagation(".enter_sends", { placement: "top", onShow(instance) { on_show_prep(instance); instance.setContent( parse_html( render_compose_select_enter_behaviour_popover({ enter_sends_true: user_settings.enter_sends, }), ), ); compose_enter_sends_popover_displayed = true; }, onMount(instance) { common.adjust_mac_shortcuts(".enter_sends_choices kbd"); $(instance.popper).one("click", ".enter_sends_choice", (e) => { let selected_behaviour = $(e.currentTarget) .find("input[type='radio']") .attr("value"); selected_behaviour = selected_behaviour === "true"; // Convert to bool user_settings.enter_sends = selected_behaviour; $(`.enter_sends_${!selected_behaviour}`).hide(); $(`.enter_sends_${selected_behaviour}`).show(); // Refocus in the content box so you can continue typing or // press Enter to send. $("#compose-textarea").trigger("focus"); channel.patch({ url: "/json/settings", data: {enter_sends: selected_behaviour}, }); e.stopPropagation(); instance.hide(); }); }, onHidden(instance) { instance.destroy(); compose_enter_sends_popover_displayed = false; }, }); }