hotspots: Convert module to TypeScript.

This commit is contained in:
afeefuddin 2024-02-29 02:06:56 +05:30 committed by Tim Abbott
parent 6314139d4a
commit 72681c2d5a
5 changed files with 94 additions and 54 deletions

View File

@ -30,7 +30,7 @@ ALL_HOTSPOTS = {
### Step 2: Configure hotspot placement ### Step 2: Configure hotspot placement
The target element and visual orientation of each hotspot is specified in The target element and visual orientation of each hotspot is specified in
`HOTSPOT_LOCATIONS` of `web/src/hotspots.js`. `HOTSPOT_LOCATIONS` of `web/src/hotspots.ts`.
The `icon_offset` property specifies where the pulsing icon is placed _relative to The `icon_offset` property specifies where the pulsing icon is placed _relative to
the width and height of the target element_. the width and height of the target element_.

View File

@ -113,7 +113,7 @@ EXEMPT_FILES = make_set(
"web/src/hashchange.js", "web/src/hashchange.js",
"web/src/hbs.d.ts", "web/src/hbs.d.ts",
"web/src/hotkey.js", "web/src/hotkey.js",
"web/src/hotspots.js", "web/src/hotspots.ts",
"web/src/inbox_ui.js", "web/src/inbox_ui.js",
"web/src/inbox_util.ts", "web/src/inbox_util.ts",
"web/src/info_overlay.js", "web/src/info_overlay.js",

View File

@ -1,5 +1,6 @@
import $ from "jquery"; import $ from "jquery";
import _ from "lodash"; import _ from "lodash";
import assert from "minimalistic-assert";
import whale_image from "../images/hotspots/whale.svg"; import whale_image from "../images/hotspots/whale.svg";
import render_hotspot_icon from "../templates/hotspot_icon.hbs"; import render_hotspot_icon from "../templates/hotspot_icon.hbs";
@ -10,6 +11,7 @@ import * as message_viewport from "./message_viewport";
import * as onboarding_steps from "./onboarding_steps"; import * as onboarding_steps from "./onboarding_steps";
import * as overlays from "./overlays"; import * as overlays from "./overlays";
import {current_user} from "./state_data"; import {current_user} from "./state_data";
import type {Hotspot, HotspotLocation, Placement, RawHotspot} from "./state_data";
// popover orientations // popover orientations
const TOP = "top"; const TOP = "top";
@ -21,7 +23,7 @@ const VIEWPORT_CENTER = "viewport_center";
// popover orientation can optionally be fixed here to override the // popover orientation can optionally be fixed here to override the
// defaults calculated by compute_placement. // defaults calculated by compute_placement.
const HOTSPOT_LOCATIONS = new Map([ const HOTSPOT_LOCATIONS = new Map<string, HotspotLocation>([
[ [
"intro_streams", "intro_streams",
{ {
@ -57,27 +59,37 @@ const HOTSPOT_LOCATIONS = new Map([
], ],
]); ]);
const meta = { const meta: {
opened_hotspot_name: null | string;
} = {
opened_hotspot_name: null, opened_hotspot_name: null,
}; };
function compute_placement($elt, popover_height, popover_width, prefer_vertical_positioning) { function compute_placement(
const client_rect = $elt.get(0).getBoundingClientRect(); $elt: JQuery,
popover_height: number,
popover_width: number,
prefer_vertical_positioning: boolean,
): Placement {
const client_rect = $elt.get(0)!.getBoundingClientRect();
const distance_from_top = client_rect.top; const distance_from_top = client_rect.top;
const distance_from_bottom = message_viewport.height() - client_rect.bottom; const distance_from_bottom = message_viewport.height() - client_rect.bottom;
const distance_from_left = client_rect.left; const distance_from_left = client_rect.left;
const distance_from_right = message_viewport.width() - client_rect.right; const distance_from_right = message_viewport.width() - client_rect.right;
const element_width = $elt.width()!;
const element_height = $elt.height()!;
const elt_will_fit_horizontally = const elt_will_fit_horizontally =
distance_from_left + $elt.width() / 2 > popover_width / 2 && distance_from_left + element_width / 2 > popover_width / 2 &&
distance_from_right + $elt.width() / 2 > popover_width / 2; distance_from_right + element_width / 2 > popover_width / 2;
const elt_will_fit_vertically = const elt_will_fit_vertically =
distance_from_bottom + $elt.height() / 2 > popover_height / 2 && distance_from_bottom + element_height / 2 > popover_height / 2 &&
distance_from_top + $elt.height() / 2 > popover_height / 2; distance_from_top + element_height / 2 > popover_height / 2;
// default to placing the popover in the center of the screen // default to placing the popover in the center of the screen
let placement = "viewport_center"; let placement: Placement = "viewport_center";
// prioritize left/right over top/bottom // prioritize left/right over top/bottom
if (distance_from_top > popover_height && elt_will_fit_horizontally) { if (distance_from_top > popover_height && elt_will_fit_horizontally) {
@ -103,11 +115,11 @@ function compute_placement($elt, popover_height, popover_width, prefer_vertical_
return placement; return placement;
} }
export function post_hotspot_as_read(hotspot_name) { export function post_hotspot_as_read(hotspot_name: string): void {
onboarding_steps.post_onboarding_step_as_read(hotspot_name); onboarding_steps.post_onboarding_step_as_read(hotspot_name);
} }
function place_icon(hotspot) { function place_icon(hotspot: Hotspot): boolean {
const $element = $(hotspot.location.element); const $element = $(hotspot.location.element);
const $icon = $(`#hotspot_${CSS.escape(hotspot.name)}_icon`); const $icon = $(`#hotspot_${CSS.escape(hotspot.name)}_icon`);
@ -122,10 +134,10 @@ function place_icon(hotspot) {
} }
const offset = { const offset = {
top: $element.outerHeight() * hotspot.location.offset_y, top: $element.outerHeight()! * hotspot.location.offset_y,
left: $element.outerWidth() * hotspot.location.offset_x, left: $element.outerWidth()! * hotspot.location.offset_x,
}; };
const client_rect = $element.get(0).getBoundingClientRect(); const client_rect = $element.get(0)!.getBoundingClientRect();
const placement = { const placement = {
top: client_rect.top + offset.top, top: client_rect.top + offset.top,
left: client_rect.left + offset.left, left: client_rect.left + offset.left,
@ -135,25 +147,22 @@ function place_icon(hotspot) {
return true; return true;
} }
function place_popover(hotspot) { function place_popover(hotspot: Hotspot): void {
if (!hotspot.location.element) {
return;
}
const popover_width = $( const popover_width = $(
`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`, `#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`,
).outerWidth(); ).outerWidth()!;
const popover_height = $( const popover_height = $(
`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`, `#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`,
).outerHeight(); ).outerHeight()!;
const el_width = $(hotspot.location.element).outerWidth(); const el_width = $(hotspot.location.element).outerWidth()!;
const el_height = $(hotspot.location.element).outerHeight(); const el_height = $(hotspot.location.element).outerHeight()!;
const arrow_offset = 20; const arrow_offset = 20;
let popover_offset; let popover_offset;
let arrow_placement; let arrow_placement;
const orientation = const orientation =
hotspot.location.popover || hotspot.location.popover ??
compute_placement($(hotspot.location.element), popover_height, popover_width, false); compute_placement($(hotspot.location.element), popover_height, popover_width, false);
switch (orientation) { switch (orientation) {
@ -207,7 +216,7 @@ function place_popover(hotspot) {
default: default:
blueslip.error("Invalid popover placement value for hotspot", {name: hotspot.name}); blueslip.error("Invalid popover placement value for hotspot", {name: hotspot.name});
break; return;
} }
// position arrow // position arrow
@ -225,7 +234,7 @@ function place_popover(hotspot) {
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
}; };
} else { } else {
const client_rect = $(hotspot.location.element).get(0).getBoundingClientRect(); const client_rect = $(hotspot.location.element).get(0)!.getBoundingClientRect();
popover_placement = { popover_placement = {
top: client_rect.top + popover_offset.top, top: client_rect.top + popover_offset.top,
left: client_rect.left + popover_offset.left, left: client_rect.left + popover_offset.left,
@ -236,7 +245,7 @@ function place_popover(hotspot) {
$(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`).css(popover_placement); $(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`).css(popover_placement);
} }
function insert_hotspot_into_DOM(hotspot) { function insert_hotspot_into_DOM(hotspot: Hotspot): void {
const hotspot_overlay_HTML = render_hotspot_overlay({ const hotspot_overlay_HTML = render_hotspot_overlay({
name: hotspot.name, name: hotspot.name,
title: hotspot.title, title: hotspot.title,
@ -272,15 +281,15 @@ function insert_hotspot_into_DOM(hotspot) {
}, hotspot.delay * 1000); }, hotspot.delay * 1000);
} }
export function is_open() { export function is_open(): boolean {
return meta.opened_hotspot_name !== null; return meta.opened_hotspot_name !== null;
} }
function is_hotspot_displayed(hotspot_name) { function is_hotspot_displayed(hotspot_name: string): number {
return $(`#hotspot_${hotspot_name}_overlay`).length; return $(`#hotspot_${hotspot_name}_overlay`).length;
} }
export function close_hotspot_icon(elem) { export function close_hotspot_icon(elem: JQuery): void {
$(elem).animate( $(elem).animate(
{opacity: 0}, {opacity: 0},
{ {
@ -292,7 +301,7 @@ export function close_hotspot_icon(elem) {
); );
} }
function close_read_hotspots(new_hotspots) { function close_read_hotspots(new_hotspots: RawHotspot[]): void {
const unwanted_hotspots = _.difference( const unwanted_hotspots = _.difference(
[...HOTSPOT_LOCATIONS.keys()], [...HOTSPOT_LOCATIONS.keys()],
new_hotspots.map((hotspot) => hotspot.name), new_hotspots.map((hotspot) => hotspot.name),
@ -304,14 +313,17 @@ function close_read_hotspots(new_hotspots) {
} }
} }
export function open_popover_if_hotspot_exist(hotspot_name, bind_element = null) { export function open_popover_if_hotspot_exist(
hotspot_name: string,
bind_element: HTMLElement,
): void {
const overlay_name = "hotspot_" + hotspot_name + "_overlay"; const overlay_name = "hotspot_" + hotspot_name + "_overlay";
if (is_hotspot_displayed(hotspot_name)) { if (is_hotspot_displayed(hotspot_name)) {
overlays.open_overlay({ overlays.open_overlay({
name: overlay_name, name: overlay_name,
$overlay: $(`#${CSS.escape(overlay_name)}`), $overlay: $(`#${CSS.escape(overlay_name)}`),
on_close: function () { on_close: function (this: HTMLElement) {
// close popover // close popover
$(this).css({display: "block"}); $(this).css({display: "block"});
$(this).animate( $(this).animate(
@ -325,17 +337,22 @@ export function open_popover_if_hotspot_exist(hotspot_name, bind_element = null)
} }
} }
export function load_new(new_hotspots) { export function load_new(new_hotspots: RawHotspot[]): void {
close_read_hotspots(new_hotspots); close_read_hotspots(new_hotspots);
let hotspot_with_location: Hotspot;
for (const hotspot of new_hotspots) { for (const hotspot of new_hotspots) {
hotspot.location = HOTSPOT_LOCATIONS.get(hotspot.name); hotspot_with_location = {
if (!is_hotspot_displayed(hotspot.name) && hotspot.location) { ...hotspot,
insert_hotspot_into_DOM(hotspot); location: HOTSPOT_LOCATIONS.get(hotspot.name)!,
};
if (!is_hotspot_displayed(hotspot.name)) {
insert_hotspot_into_DOM(hotspot_with_location);
} }
} }
} }
export function initialize() { export function initialize(): void {
load_new(onboarding_steps.filter_new_hotspots(current_user.onboarding_steps)); load_new(onboarding_steps.filter_new_hotspots(current_user.onboarding_steps));
// open // open
@ -344,10 +361,12 @@ export function initialize() {
close_hotspot_icon(this); close_hotspot_icon(this);
// show popover // show popover
const [, hotspot_name] = /^hotspot_(.*)_icon$/.exec( const match_array = /^hotspot_(.*)_icon$/.exec(
$(e.target).closest(".hotspot-icon").attr("id"), $(e.target).closest(".hotspot-icon").attr("id")!,
); );
assert(match_array !== null);
const [, hotspot_name] = match_array;
open_popover_if_hotspot_exist(hotspot_name, this); open_popover_if_hotspot_exist(hotspot_name, this);
meta.opened_hotspot_name = hotspot_name; meta.opened_hotspot_name = hotspot_name;
@ -360,9 +379,11 @@ export function initialize() {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const overlay_name = $(this).closest(".hotspot.overlay").attr("id"); const overlay_name = $(this).closest(".hotspot.overlay").attr("id")!;
const [, hotspot_name] = /^hotspot_(.*)_overlay$/.exec(overlay_name); const match_array = /^hotspot_(.*)_overlay$/.exec(overlay_name);
assert(match_array !== null);
const [, hotspot_name] = match_array;
// Comment below to disable marking hotspots as read in production // Comment below to disable marking hotspots as read in production
post_hotspot_as_read(hotspot_name); post_hotspot_as_read(hotspot_name);

View File

@ -1,7 +1,7 @@
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import * as channel from "./channel"; import * as channel from "./channel";
import {current_user} from "./state_data"; import {current_user} from "./state_data";
import type {OnboardingStep} from "./state_data"; import type {OnboardingStep, RawHotspot} from "./state_data";
export const ONE_TIME_NOTICES_TO_DISPLAY = new Set<string>(); export const ONE_TIME_NOTICES_TO_DISPLAY = new Set<string>();
@ -21,8 +21,10 @@ export function post_onboarding_step_as_read(onboarding_step_name: string): void
}); });
} }
export function filter_new_hotspots(onboarding_steps: OnboardingStep[]): OnboardingStep[] { export function filter_new_hotspots(onboarding_steps: OnboardingStep[]): RawHotspot[] {
return onboarding_steps.filter((onboarding_step) => onboarding_step.type === "hotspot"); return onboarding_steps.flatMap((onboarding_step) =>
onboarding_step.type === "hotspot" ? [onboarding_step] : [],
);
} }
export function update_notice_to_display(onboarding_steps: OnboardingStep[]): void { export function update_notice_to_display(onboarding_steps: OnboardingStep[]): void {

View File

@ -21,32 +21,49 @@ export const narrow_term_schema = z.object({
export type NarrowTerm = z.output<typeof narrow_term_schema>; export type NarrowTerm = z.output<typeof narrow_term_schema>;
// Sync this with zerver.lib.events.do_events_register. // Sync this with zerver.lib.events.do_events_register.
const placement_schema = z.enum([
"top",
"left",
"right",
"bottom",
"left_bottom",
"viewport_center",
]);
export type Placement = z.infer<typeof placement_schema>;
const hotspot_location_schema = z.object({ const hotspot_location_schema = z.object({
element: z.string(), element: z.string(),
offset_x: z.number(), offset_x: z.number(),
offset_y: z.number(), offset_y: z.number(),
popover: z.optional(z.string()), popover: z.optional(placement_schema),
}); });
const hotspot_schema = z.object({ export type HotspotLocation = z.output<typeof hotspot_location_schema>;
const raw_hotspot_schema = z.object({
delay: z.number(), delay: z.number(),
description: z.string(), description: z.string(),
has_trigger: z.boolean(), has_trigger: z.boolean(),
location: z.optional(hotspot_location_schema),
name: z.string(), name: z.string(),
title: z.string(), title: z.string(),
type: z.literal("hotspot"), type: z.literal("hotspot"),
}); });
export type RawHotspot = z.output<typeof raw_hotspot_schema>;
const hotspot_schema = raw_hotspot_schema.extend({
location: hotspot_location_schema,
});
export type Hotspot = z.output<typeof hotspot_schema>;
const one_time_notice_schema = z.object({ const one_time_notice_schema = z.object({
name: z.string(), name: z.string(),
type: z.literal("one_time_notice"), type: z.literal("one_time_notice"),
}); });
const onboarding_step_schema = z.discriminatedUnion("type", [ const onboarding_step_schema = z.union([one_time_notice_schema, raw_hotspot_schema]);
one_time_notice_schema,
hotspot_schema,
]);
export type OnboardingStep = z.output<typeof onboarding_step_schema>; export type OnboardingStep = z.output<typeof onboarding_step_schema>;