mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	Modals are closed when selecting something inside the modal if mouse is dragged outside the modal. This happens as leaving the mouse after dragging it outside the modal induces a click even outside the modal and the current behavior is to close the modal on clicking outside it. We remove the "data-micromodal-close" attribute from "modal__overlay" element which is responsible for closing the modal. And we add click handler which closes the modal on clicking outside the modal but does not closes it when the mouse is dragged outside while selecting something. Fixes #23350.
		
			
				
	
	
		
			344 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			344 lines
		
	
	
		
			10 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() {
 | 
						|
    return $(".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";
 | 
						|
}
 | 
						|
 | 
						|
export function active_modal() {
 | 
						|
    if (!is_modal_open()) {
 | 
						|
        blueslip.error("Programming error — Called active_modal when there is no modal open");
 | 
						|
        return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    const $micromodal = $(".micromodal.modal--open");
 | 
						|
    return `#${CSS.escape($micromodal.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.
 | 
						|
// 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;
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't accept hash-based selector to enforce modals to have unique ids and
 | 
						|
    // since micromodal doesn't accept hash based selectors.
 | 
						|
    if (selector[0] === "#") {
 | 
						|
        blueslip.error("hash-based selector passed in to open_modal: " + selector);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (is_modal_open()) {
 | 
						|
        /*
 | 
						|
          Our modal system doesn't directly support opening a modal
 | 
						|
          when one is already open, because the `is_modal_open` CSS
 | 
						|
          class doesn't update until Micromodal has finished its
 | 
						|
          animations, which can take 100ms or more.
 | 
						|
 | 
						|
          We can likely fix that, but in the meantime, we should
 | 
						|
          handle this situation correctly, by closing the current
 | 
						|
          modal, waiting for it to finish closing, and then attempting
 | 
						|
          to open the current modal again.
 | 
						|
        */
 | 
						|
        if (!conf.recursive_call_count) {
 | 
						|
            conf.recursive_call_count = 1;
 | 
						|
        } else {
 | 
						|
            conf.recursive_call_count += 1;
 | 
						|
        }
 | 
						|
        if (conf.recursive_call_count > 50) {
 | 
						|
            blueslip.error("Modal incorrectly is still open: " + selector);
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        close_active_modal();
 | 
						|
        setTimeout(() => {
 | 
						|
            open_modal(selector, conf);
 | 
						|
        }, 10);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    blueslip.debug("open modal: " + selector);
 | 
						|
 | 
						|
    // 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) => {
 | 
						|
        const animation_name = event.originalEvent.animationName;
 | 
						|
        if (animation_name === "mmfadeIn") {
 | 
						|
            // 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") {
 | 
						|
            // 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.find(".modal__overlay").on("click", (e) => {
 | 
						|
        /* Micromodal's data-micromodal-close feature doesn't check for
 | 
						|
           range selections; this means dragging a selection of text in an
 | 
						|
           input inside the modal too far will weirdly close the modal.
 | 
						|
           See https://github.com/ghosh/Micromodal/issues/505.
 | 
						|
           Work around this with our own implementation. */
 | 
						|
        if (!$(e.target).is(".modal__overlay")) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (document.getSelection().type === "Range") {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        close_modal(selector);
 | 
						|
    });
 | 
						|
 | 
						|
    Micromodal.show(selector, {
 | 
						|
        disableFocus: true,
 | 
						|
        openClass: "modal--opening",
 | 
						|
        onShow: conf?.on_show,
 | 
						|
        onClose: conf?.on_hide,
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
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:
 | 
						|
// * 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 (active_modal() !== `#${selector}`) {
 | 
						|
        blueslip.error(
 | 
						|
            "Trying to close " + selector + " modal when " + active_modal() + " is open.",
 | 
						|
        );
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    blueslip.debug("close modal: " + selector);
 | 
						|
 | 
						|
    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);
 | 
						|
}
 | 
						|
 | 
						|
export function close_active_modal() {
 | 
						|
    if (!is_modal_open()) {
 | 
						|
        blueslip.warn("close_active_modal() called without checking is_modal_open()");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const $micromodal = $(".micromodal.modal--open");
 | 
						|
    Micromodal.close(`${CSS.escape($micromodal.attr("id"))}`);
 | 
						|
}
 | 
						|
 | 
						|
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();
 | 
						|
    });
 | 
						|
}
 |