mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	This applies the fixes we have when restoring scroll position and marking messages as read in background for overlays to modals.
		
			
				
	
	
		
			375 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			375 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import $ from "jquery";
 | 
						|
import Micromodal from "micromodal";
 | 
						|
 | 
						|
import * as blueslip from "./blueslip";
 | 
						|
import * as browser_history from "./browser_history";
 | 
						|
import * as popovers from "./popovers";
 | 
						|
 | 
						|
let $active_overlay;
 | 
						|
let close_handler;
 | 
						|
let open_overlay_name;
 | 
						|
 | 
						|
function reset_state() {
 | 
						|
    $active_overlay = undefined;
 | 
						|
    close_handler = undefined;
 | 
						|
    open_overlay_name = undefined;
 | 
						|
}
 | 
						|
 | 
						|
export function is_active() {
 | 
						|
    return Boolean(open_overlay_name);
 | 
						|
}
 | 
						|
 | 
						|
export function is_modal_open() {
 | 
						|
    // Check for both Bootstrap and Micromodal modals.
 | 
						|
    return $(".modal").hasClass("in") || $(".micromodal").hasClass("modal--open");
 | 
						|
}
 | 
						|
 | 
						|
export function is_overlay_or_modal_open() {
 | 
						|
    return is_active() || is_modal_open();
 | 
						|
}
 | 
						|
 | 
						|
export function info_overlay_open() {
 | 
						|
    return open_overlay_name === "informationalOverlays";
 | 
						|
}
 | 
						|
 | 
						|
export function settings_open() {
 | 
						|
    return open_overlay_name === "settings";
 | 
						|
}
 | 
						|
 | 
						|
export function streams_open() {
 | 
						|
    return open_overlay_name === "subscriptions";
 | 
						|
}
 | 
						|
 | 
						|
export function lightbox_open() {
 | 
						|
    return open_overlay_name === "lightbox";
 | 
						|
}
 | 
						|
 | 
						|
export function drafts_open() {
 | 
						|
    return open_overlay_name === "drafts";
 | 
						|
}
 | 
						|
 | 
						|
// To address bugs where mouse might apply to the streams/settings
 | 
						|
// overlays underneath an open modal within those settings UI, we add
 | 
						|
// this inline style to 'div.overlay.show', overriding the
 | 
						|
// "pointer-events: all" style in app_components.css.
 | 
						|
//
 | 
						|
// This is kinda hacky; it only works for modals within overlays, and
 | 
						|
// we need to make sure it gets re-enabled when the modal closes.
 | 
						|
export function disable_background_mouse_events() {
 | 
						|
    $("div.overlay.show").attr("style", "pointer-events: none");
 | 
						|
}
 | 
						|
 | 
						|
// This removes only the inline-style of the element that
 | 
						|
// was added in disable_background_mouse_events and
 | 
						|
// enables the background mouse events.
 | 
						|
export function enable_background_mouse_events() {
 | 
						|
    $("div.overlay.show").attr("style", null);
 | 
						|
}
 | 
						|
 | 
						|
export function active_modal() {
 | 
						|
    if (!is_modal_open()) {
 | 
						|
        blueslip.error("Programming error — Called active_modal when there is no modal open");
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    // Check for Micromodal modals.
 | 
						|
    const $micromodal = $(".micromodal.modal--open");
 | 
						|
    if ($micromodal.length) {
 | 
						|
        return `#${CSS.escape($micromodal.attr("id"))}`;
 | 
						|
    }
 | 
						|
    return `#${CSS.escape($(".modal.in").attr("id"))}`;
 | 
						|
}
 | 
						|
 | 
						|
export function open_overlay(opts) {
 | 
						|
    popovers.hide_all();
 | 
						|
 | 
						|
    if (!opts.name || !opts.$overlay || !opts.on_close) {
 | 
						|
        blueslip.error("Programming error in open_overlay");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if ($active_overlay || open_overlay_name || close_handler) {
 | 
						|
        blueslip.error(
 | 
						|
            "Programming error — trying to open " +
 | 
						|
                opts.name +
 | 
						|
                " before closing " +
 | 
						|
                open_overlay_name,
 | 
						|
        );
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    blueslip.debug("open overlay: " + opts.name);
 | 
						|
 | 
						|
    // Our overlays are kind of crufty...we have an HTML id
 | 
						|
    // attribute for them and then a data-overlay attribute for
 | 
						|
    // them.  Make sure they match.
 | 
						|
    if (opts.$overlay.attr("data-overlay") !== opts.name) {
 | 
						|
        blueslip.error("Bad overlay setup for " + opts.name);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    open_overlay_name = opts.name;
 | 
						|
    $active_overlay = opts.$overlay;
 | 
						|
    opts.$overlay.addClass("show");
 | 
						|
 | 
						|
    opts.$overlay.attr("aria-hidden", "false");
 | 
						|
    $(".app").attr("aria-hidden", "true");
 | 
						|
    $(".fixed-app").attr("aria-hidden", "true");
 | 
						|
    $(".header").attr("aria-hidden", "true");
 | 
						|
 | 
						|
    close_handler = function () {
 | 
						|
        opts.on_close();
 | 
						|
        reset_state();
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
// If conf.autoremove is true, the modal element will be removed from the DOM
 | 
						|
// once the modal is hidden.
 | 
						|
// If conf.micromodal is true, open a micromodal modal else open a bootstrap modal
 | 
						|
// conf also accepts the following optional properties:
 | 
						|
// on_show: Callback to run when the modal is triggered to show.
 | 
						|
// on_shown: Callback to run when the modal is shown.
 | 
						|
// on_hide: Callback to run when the modal is triggered to hide.
 | 
						|
// on_hidden: Callback to run when the modal is hidden.
 | 
						|
export function open_modal(selector, conf) {
 | 
						|
    if (selector === undefined) {
 | 
						|
        blueslip.error("Undefined selector was passed into open_modal");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if ((!conf || (conf && !conf.micromodal)) && selector[0] !== "#") {
 | 
						|
        blueslip.error("Non-id-based selector passed in to open_modal: " + selector);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't accept hash-based selector to enforce modals to have unique ids and
 | 
						|
    // since micromodal doesn't accept hash based selectors.
 | 
						|
    if (conf && conf.micromodal && selector[0] === "#") {
 | 
						|
        blueslip.error("hash-based selector passed in to micromodal-based open_modal: " + selector);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (is_modal_open()) {
 | 
						|
        blueslip.error("open_modal() was called while " + active_modal() + " modal was open.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    blueslip.debug("open modal: " + selector);
 | 
						|
 | 
						|
    // Show a modal using micromodal.
 | 
						|
    if (conf && conf.micromodal) {
 | 
						|
        // Micromodal gets elements using the getElementById DOM function
 | 
						|
        // which doesn't require the hash. We add it manually here.
 | 
						|
        const id_selector = `#${selector}`;
 | 
						|
        const $micromodal = $(id_selector);
 | 
						|
 | 
						|
        $micromodal.find(".modal__container").on("animationend", (event) => {
 | 
						|
            // Micromodal doesn't support Bootstrap-style `shown.bs.modal` and
 | 
						|
            // `hidden.bs.modal` events. We workaround this by using the animationName
 | 
						|
            // from the native event and running the required functions after the
 | 
						|
            // animation ends.
 | 
						|
            const animation_name = event.originalEvent.animationName;
 | 
						|
            if (animation_name === "mmfadeIn") {
 | 
						|
                // Equivalent to bootstrap's "shown.bs.modal" event
 | 
						|
 | 
						|
                // Micromodal adds the is-open class before the modal animation
 | 
						|
                // is complete, which isn't really helpful since a modal is open after the
 | 
						|
                // animation is complete. So, we manually add a class after the
 | 
						|
                // animation is complete.
 | 
						|
                $micromodal.addClass("modal--open");
 | 
						|
                $micromodal.removeClass("modal--opening");
 | 
						|
 | 
						|
                if (conf.on_shown) {
 | 
						|
                    conf.on_shown();
 | 
						|
                }
 | 
						|
            } else if (animation_name === "mmfadeOut") {
 | 
						|
                // Equivalent to bootstrap's "hidden.bs.modal" event.
 | 
						|
                //
 | 
						|
                // Call the on_hidden callback after the modal finishes hiding.
 | 
						|
 | 
						|
                $micromodal.removeClass("modal--open");
 | 
						|
                if (conf.autoremove) {
 | 
						|
                    $micromodal.remove();
 | 
						|
                }
 | 
						|
                if (conf.on_hidden) {
 | 
						|
                    conf.on_hidden();
 | 
						|
                }
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        Micromodal.show(selector, {
 | 
						|
            disableFocus: true,
 | 
						|
            openClass: "modal--opening",
 | 
						|
            onShow: conf?.on_show,
 | 
						|
            onClose: conf?.on_hide,
 | 
						|
        });
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const $elem = $(selector).expectOne();
 | 
						|
    $elem.modal("show").attr("aria-hidden", false);
 | 
						|
    // Disable background mouse events when modal is active
 | 
						|
    disable_background_mouse_events();
 | 
						|
    // Remove previous alert messages from modal, if exists.
 | 
						|
    $elem.find(".alert").hide();
 | 
						|
    $elem.find(".alert-notification").html("");
 | 
						|
 | 
						|
    if (conf && conf.autoremove) {
 | 
						|
        $elem.on("hidden.bs.modal", () => {
 | 
						|
            $elem.remove();
 | 
						|
        });
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function close_overlay(name) {
 | 
						|
    popovers.hide_all();
 | 
						|
 | 
						|
    if (name !== open_overlay_name) {
 | 
						|
        blueslip.error("Trying to close " + name + " when " + open_overlay_name + " is open.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (name === undefined) {
 | 
						|
        blueslip.error("Undefined name was passed into close_overlay");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    blueslip.debug("close overlay: " + name);
 | 
						|
 | 
						|
    $active_overlay.removeClass("show");
 | 
						|
 | 
						|
    $active_overlay.attr("aria-hidden", "true");
 | 
						|
    $(".app").attr("aria-hidden", "false");
 | 
						|
    $(".fixed-app").attr("aria-hidden", "false");
 | 
						|
    $(".header").attr("aria-hidden", "false");
 | 
						|
 | 
						|
    if (!close_handler) {
 | 
						|
        blueslip.error("Overlay close handler for " + name + " not properly set up.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    close_handler();
 | 
						|
}
 | 
						|
 | 
						|
export function close_active() {
 | 
						|
    if (!open_overlay_name) {
 | 
						|
        blueslip.warn("close_active() called without checking is_active()");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    close_overlay(open_overlay_name);
 | 
						|
}
 | 
						|
 | 
						|
// `conf` is an object with the following optional properties:
 | 
						|
// * micromodal: true if the modal to close is expected to be micromodal.
 | 
						|
// * on_hidden: Callback to run when the modal finishes hiding.
 | 
						|
export function close_modal(selector, conf) {
 | 
						|
    if (selector === undefined) {
 | 
						|
        blueslip.error("Undefined selector was passed into close_modal");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!is_modal_open()) {
 | 
						|
        blueslip.warn("close_active_modal() called without checking is_modal_open()");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (
 | 
						|
        (!conf && active_modal() !== selector) ||
 | 
						|
        (conf && conf.micromodal && active_modal() !== `#${selector}`)
 | 
						|
    ) {
 | 
						|
        blueslip.error(
 | 
						|
            "Trying to close " + selector + " modal when " + active_modal() + " is open.",
 | 
						|
        );
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    blueslip.debug("close modal: " + selector);
 | 
						|
 | 
						|
    if (conf && conf.micromodal) {
 | 
						|
        const id_selector = `#${selector}`;
 | 
						|
        const $micromodal = $(id_selector);
 | 
						|
 | 
						|
        // On-hidden hooks should typically be registered in
 | 
						|
        // overlays.open_modal.  However, we offer this alternative
 | 
						|
        // mechanism as a convenience for hooks only known when
 | 
						|
        // closing the modal.
 | 
						|
        $micromodal.find(".modal__container").on("animationend", (event) => {
 | 
						|
            const animation_name = event.originalEvent.animationName;
 | 
						|
            if (animation_name === "mmfadeOut" && conf.on_hidden) {
 | 
						|
                conf.on_hidden();
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        Micromodal.close(selector);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const $elem = $(selector).expectOne();
 | 
						|
    $elem.modal("hide").attr("aria-hidden", true);
 | 
						|
}
 | 
						|
 | 
						|
export function close_active_modal() {
 | 
						|
    if (!is_modal_open()) {
 | 
						|
        blueslip.warn("close_active_modal() called without checking is_modal_open()");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Check for Micromodal modals.
 | 
						|
    const $micromodal = $(".micromodal.modal--open");
 | 
						|
    if ($micromodal.length) {
 | 
						|
        Micromodal.close(`${CSS.escape($micromodal.attr("id"))}`);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    $(".modal.in").modal("hide").attr("aria-hidden", true);
 | 
						|
}
 | 
						|
 | 
						|
export function close_for_hash_change() {
 | 
						|
    $("div.overlay.show").removeClass("show");
 | 
						|
    if ($active_overlay) {
 | 
						|
        close_handler();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export function open_settings() {
 | 
						|
    open_overlay({
 | 
						|
        name: "settings",
 | 
						|
        $overlay: $("#settings_overlay_container"),
 | 
						|
        on_close() {
 | 
						|
            browser_history.exit_overlay();
 | 
						|
        },
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function initialize() {
 | 
						|
    $("body").on("click", "div.overlay, div.overlay .exit", (e) => {
 | 
						|
        let $target = $(e.target);
 | 
						|
 | 
						|
        if (document.getSelection().type === "Range") {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // if the target is not the div.overlay element, search up the node tree
 | 
						|
        // until it is found.
 | 
						|
        if ($target.is(".exit, .exit-sign, .overlay-content, .exit span")) {
 | 
						|
            $target = $target.closest("[data-overlay]");
 | 
						|
        } else if (!$target.is("div.overlay")) {
 | 
						|
            // not a valid click target then.
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($target.data("noclose")) {
 | 
						|
            // This overlay has been marked explicitly to not be closed.
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        const target_name = $target.attr("data-overlay");
 | 
						|
 | 
						|
        close_overlay(target_name);
 | 
						|
 | 
						|
        e.preventDefault();
 | 
						|
        e.stopPropagation();
 | 
						|
    });
 | 
						|
}
 |