ts: Convert `overlays.js` to TypeScript.

Refactored some code as well to avoid unneccesaary `undefined`
checks in `overlays`. To be exact created an action_overlay
object of type `Overlay` such that the `reset_state` is just
`action_overlay = undefined` and having attributes like `$element`
and `close_handler`.

This would ensure that if you have an `active_overlay`, there will
be a registered `close handler` attached to it without needing to
write additional checks for `close_handler` just to satisfy type
checker.
This commit is contained in:
Lalit 2023-03-29 20:04:02 +05:30 committed by Tim Abbott
parent 0de0743e91
commit fcbc6dcae4
2 changed files with 88 additions and 65 deletions

View File

@ -123,7 +123,7 @@ EXEMPT_FILES = make_set(
"web/src/navbar_alerts.js",
"web/src/navigate.js",
"web/src/notifications.js",
"web/src/overlays.js",
"web/src/overlays.ts",
"web/src/padded_widget.ts",
"web/src/page_params.ts",
"web/src/pm_list.js",

View File

@ -3,80 +3,99 @@ import Micromodal from "micromodal";
import * as blueslip from "./blueslip";
let $active_overlay;
let close_handler;
let open_overlay_name;
type Hook = () => void;
const pre_open_hooks = [];
const pre_close_hooks = [];
type OverlayOptions = {
name: string;
$overlay: JQuery<HTMLElement>;
on_close: () => void;
};
function reset_state() {
$active_overlay = undefined;
close_handler = undefined;
type Overlay = {
$element: JQuery<HTMLElement>;
close_handler: () => void;
};
export type ModalConfig = {
autoremove?: boolean;
on_show?: () => void;
on_shown?: () => void;
on_hide?: () => void;
on_hidden?: () => void;
};
let active_overlay: Overlay | undefined;
let open_overlay_name: string | undefined;
const pre_open_hooks: Hook[] = [];
const pre_close_hooks: Hook[] = [];
function reset_state(): void {
active_overlay = undefined;
open_overlay_name = undefined;
}
export function register_pre_open_hook(func) {
export function register_pre_open_hook(func: Hook): void {
pre_open_hooks.push(func);
}
export function register_pre_close_hook(func) {
export function register_pre_close_hook(func: Hook): void {
pre_close_hooks.push(func);
}
function call_hooks(func_list) {
function call_hooks(func_list: Hook[]): void {
for (const element of func_list) {
element();
}
}
export function is_active() {
export function is_active(): boolean {
return Boolean(open_overlay_name);
}
export function is_modal_open() {
export function is_modal_open(): boolean {
return $(".micromodal").hasClass("modal--open");
}
export function is_overlay_or_modal_open() {
export function is_overlay_or_modal_open(): boolean {
return is_active() || is_modal_open();
}
export function info_overlay_open() {
export function info_overlay_open(): boolean {
return open_overlay_name === "informationalOverlays";
}
export function settings_open() {
export function settings_open(): boolean {
return open_overlay_name === "settings";
}
export function streams_open() {
export function streams_open(): boolean {
return open_overlay_name === "subscriptions";
}
export function groups_open() {
export function groups_open(): boolean {
return open_overlay_name === "group_subscriptions";
}
export function lightbox_open() {
export function lightbox_open(): boolean {
return open_overlay_name === "lightbox";
}
export function drafts_open() {
export function drafts_open(): boolean {
return open_overlay_name === "drafts";
}
export function active_modal() {
export function active_modal(): string | undefined {
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"))}`;
return `#${CSS.escape($micromodal.attr("id")!)}`;
}
export function open_overlay(opts) {
export function open_overlay(opts: OverlayOptions): void {
call_hooks(pre_open_hooks);
if (!opts.name || !opts.$overlay || !opts.on_close) {
@ -84,13 +103,13 @@ export function open_overlay(opts) {
return;
}
if ($active_overlay || open_overlay_name || close_handler) {
if (active_overlay || open_overlay_name) {
blueslip.error(
"Programming error — trying to open " +
opts.name +
" before closing " +
open_overlay_name,
`Programming error - trying to open ${opts.name} before closing ${
open_overlay_name || "undefined"
}`,
);
return;
}
@ -105,17 +124,18 @@ export function open_overlay(opts) {
}
open_overlay_name = opts.name;
$active_overlay = opts.$overlay;
opts.$overlay.addClass("show");
active_overlay = {
$element: opts.$overlay,
close_handler() {
opts.on_close();
reset_state();
},
};
opts.$overlay.addClass("show");
opts.$overlay.attr("aria-hidden", "false");
$(".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
@ -125,7 +145,10 @@ export function open_overlay(opts) {
// 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(modal_id, conf = {}) {
export function open_modal(
modal_id: string,
conf: ModalConfig & {recursive_call_count?: number} = {},
): void {
if (modal_id === undefined) {
blueslip.error("Undefined id was passed into open_modal");
return;
@ -133,7 +156,7 @@ export function open_modal(modal_id, conf = {}) {
// Don't accept hash-based selector to enforce modals to have unique ids and
// since micromodal doesn't accept hash based selectors.
if (modal_id[0] === "#") {
if (modal_id.startsWith("#")) {
blueslip.error("hash-based selector passed in to open_modal: " + modal_id);
return;
}
@ -175,7 +198,7 @@ export function open_modal(modal_id, conf = {}) {
const $micromodal = $(id_selector);
$micromodal.find(".modal__container").on("animationend", (event) => {
const animation_name = event.originalEvent.animationName;
const animation_name = (event.originalEvent as AnimationEvent).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
@ -210,7 +233,7 @@ export function open_modal(modal_id, conf = {}) {
return;
}
if (document.getSelection().type === "Range") {
if (document.getSelection()?.type === "Range") {
return;
}
close_modal(modal_id);
@ -224,11 +247,11 @@ export function open_modal(modal_id, conf = {}) {
});
}
export function close_overlay(name) {
export function close_overlay(name: string): void {
call_hooks(pre_close_hooks);
if (name !== open_overlay_name) {
blueslip.error("Trying to close " + name + " when " + open_overlay_name + " is open.");
blueslip.error(`Trying to close ${name} when ${open_overlay_name || "undefined"} is open.`);
return;
}
@ -237,23 +260,23 @@ export function close_overlay(name) {
return;
}
blueslip.debug("close overlay: " + name);
$active_overlay.removeClass("show");
$active_overlay.attr("aria-hidden", "true");
$(".app").attr("aria-hidden", "false");
$(".header").attr("aria-hidden", "false");
if (!close_handler) {
blueslip.error("Overlay close handler for " + name + " not properly set up.");
if (active_overlay === undefined) {
blueslip.error("close_overlay called without checking is_active()");
return;
}
close_handler();
blueslip.debug("close overlay: " + name);
active_overlay.$element.removeClass("show");
active_overlay.$element.attr("aria-hidden", "true");
$(".app").attr("aria-hidden", "false");
$(".header").attr("aria-hidden", "false");
active_overlay.close_handler();
}
export function close_active() {
export function close_active(): void {
if (!open_overlay_name) {
blueslip.warn("close_active() called without checking is_active()");
return;
@ -264,7 +287,7 @@ export function close_active() {
// `conf` is an object with the following optional properties:
// * on_hidden: Callback to run when the modal finishes hiding.
export function close_modal(modal_id, conf = {}) {
export function close_modal(modal_id: string, conf: Pick<ModalConfig, "on_hidden"> = {}): void {
if (modal_id === undefined) {
blueslip.error("Undefined id was passed into close_modal");
return;
@ -277,7 +300,7 @@ export function close_modal(modal_id, conf = {}) {
if (active_modal() !== `#${CSS.escape(modal_id)}`) {
blueslip.error(
"Trying to close " + modal_id + " modal when " + active_modal() + " is open.",
`Trying to close ${modal_id} modal when ${active_modal() || "undefined"} is open.`,
);
return;
}
@ -292,7 +315,7 @@ export function close_modal(modal_id, conf = {}) {
// 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;
const animation_name = (event.originalEvent as AnimationEvent).animationName;
if (animation_name === "mmfadeOut" && conf.on_hidden) {
conf.on_hidden();
}
@ -301,28 +324,28 @@ export function close_modal(modal_id, conf = {}) {
Micromodal.close(modal_id);
}
export function close_active_modal() {
export function close_active_modal(): void {
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"))}`);
Micromodal.close(`${CSS.escape($micromodal.attr("id") || "")}`);
}
export function close_for_hash_change() {
export function close_for_hash_change(): void {
$("div.overlay.show").removeClass("show");
if ($active_overlay) {
close_handler();
if (active_overlay) {
active_overlay.close_handler();
}
}
export function initialize() {
export function initialize(): void {
$("body").on("click", "div.overlay, div.overlay .exit", (e) => {
let $target = $(e.target);
if (document.getSelection().type === "Range") {
if (document.getSelection()?.type === "Range") {
return;
}
@ -340,7 +363,7 @@ export function initialize() {
return;
}
const target_name = $target.attr("data-overlay");
const target_name = $target.attr("data-overlay")!;
close_overlay(target_name);