mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			277 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import $ from "jquery";
 | |
| import _ from "lodash";
 | |
| 
 | |
| import render_hotspot_icon from "../templates/hotspot_icon.hbs";
 | |
| import render_hotspot_overlay from "../templates/hotspot_overlay.hbs";
 | |
| 
 | |
| import * as blueslip from "./blueslip";
 | |
| import * as channel from "./channel";
 | |
| import {page_params} from "./page_params";
 | |
| import * as popovers from "./popovers";
 | |
| 
 | |
| // popover orientations
 | |
| const TOP = "top";
 | |
| const LEFT = "left";
 | |
| const RIGHT = "right";
 | |
| const BOTTOM = "bottom";
 | |
| const LEFT_BOTTOM = "left_bottom";
 | |
| const VIEWPORT_CENTER = "viewport_center";
 | |
| 
 | |
| // popover orientation can optionally be fixed here (property: popover),
 | |
| // otherwise popovers.compute_placement is used to compute orientation
 | |
| const HOTSPOT_LOCATIONS = new Map([
 | |
|     [
 | |
|         "intro_streams",
 | |
|         {
 | |
|             element: "#streams_header .sidebar-title",
 | |
|             offset_x: 1.35,
 | |
|             offset_y: 0.39,
 | |
|         },
 | |
|     ],
 | |
|     [
 | |
|         "intro_topics",
 | |
|         {
 | |
|             element: ".topic-name",
 | |
|             offset_x: 0.8,
 | |
|             offset_y: 0.39,
 | |
|         },
 | |
|     ],
 | |
|     [
 | |
|         "intro_gear",
 | |
|         {
 | |
|             element: "#settings-dropdown",
 | |
|             offset_x: -0.4,
 | |
|             offset_y: 1.2,
 | |
|             popover: LEFT_BOTTOM,
 | |
|         },
 | |
|     ],
 | |
|     [
 | |
|         "intro_compose",
 | |
|         {
 | |
|             element: "#left_bar_compose_stream_button_big",
 | |
|             offset_x: 0,
 | |
|             offset_y: 0,
 | |
|         },
 | |
|     ],
 | |
| ]);
 | |
| 
 | |
| // popover illustration url(s)
 | |
| const WHALE = "/static/images/hotspots/whale.svg";
 | |
| 
 | |
| export function post_hotspot_as_read(hotspot_name) {
 | |
|     channel.post({
 | |
|         url: "/json/users/me/hotspots",
 | |
|         data: {hotspot: hotspot_name},
 | |
|         error(err) {
 | |
|             blueslip.error(err.responseText);
 | |
|         },
 | |
|     });
 | |
| }
 | |
| 
 | |
| function place_icon(hotspot) {
 | |
|     const $element = $(hotspot.location.element);
 | |
|     const $icon = $(`#hotspot_${CSS.escape(hotspot.name)}_icon`);
 | |
| 
 | |
|     if (
 | |
|         $element.length === 0 ||
 | |
|         $element.css("display") === "none" ||
 | |
|         !$element.is(":visible") ||
 | |
|         $element.is(":hidden")
 | |
|     ) {
 | |
|         $icon.css("display", "none");
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     const offset = {
 | |
|         top: $element.outerHeight() * hotspot.location.offset_y,
 | |
|         left: $element.outerWidth() * hotspot.location.offset_x,
 | |
|     };
 | |
|     const client_rect = $element.get(0).getBoundingClientRect();
 | |
|     const placement = {
 | |
|         top: client_rect.top + offset.top,
 | |
|         left: client_rect.left + offset.left,
 | |
|     };
 | |
|     $icon.css("display", "block");
 | |
|     $icon.css(placement);
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| function place_popover(hotspot) {
 | |
|     if (!hotspot.location.element) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const popover_width = $(
 | |
|         `#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`,
 | |
|     ).outerWidth();
 | |
|     const popover_height = $(
 | |
|         `#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`,
 | |
|     ).outerHeight();
 | |
|     const el_width = $(hotspot.location.element).outerWidth();
 | |
|     const el_height = $(hotspot.location.element).outerHeight();
 | |
|     const arrow_offset = 20;
 | |
| 
 | |
|     let popover_offset;
 | |
|     let arrow_placement;
 | |
|     const orientation =
 | |
|         hotspot.location.popover ||
 | |
|         popovers.compute_placement(
 | |
|             $(hotspot.location.element),
 | |
|             popover_height,
 | |
|             popover_width,
 | |
|             false,
 | |
|         );
 | |
| 
 | |
|     switch (orientation) {
 | |
|         case TOP:
 | |
|             popover_offset = {
 | |
|                 top: -(popover_height + arrow_offset),
 | |
|                 left: el_width / 2 - popover_width / 2,
 | |
|             };
 | |
|             arrow_placement = "bottom";
 | |
|             break;
 | |
| 
 | |
|         case LEFT:
 | |
|             popover_offset = {
 | |
|                 top: el_height / 2 - popover_height / 2,
 | |
|                 left: -(popover_width + arrow_offset),
 | |
|             };
 | |
|             arrow_placement = "right";
 | |
|             break;
 | |
| 
 | |
|         case BOTTOM:
 | |
|             popover_offset = {
 | |
|                 top: el_height + arrow_offset,
 | |
|                 left: el_width / 2 - popover_width / 2,
 | |
|             };
 | |
|             arrow_placement = "top";
 | |
|             break;
 | |
| 
 | |
|         case RIGHT:
 | |
|             popover_offset = {
 | |
|                 top: el_height / 2 - popover_height / 2,
 | |
|                 left: el_width + arrow_offset,
 | |
|             };
 | |
|             arrow_placement = "left";
 | |
|             break;
 | |
| 
 | |
|         case LEFT_BOTTOM:
 | |
|             popover_offset = {
 | |
|                 top: 0,
 | |
|                 left: -(popover_width + arrow_offset / 2),
 | |
|             };
 | |
|             arrow_placement = "";
 | |
|             break;
 | |
| 
 | |
|         case VIEWPORT_CENTER:
 | |
|             popover_offset = {
 | |
|                 top: el_height / 2,
 | |
|                 left: el_width / 2,
 | |
|             };
 | |
|             arrow_placement = "";
 | |
|             break;
 | |
| 
 | |
|         default:
 | |
|             blueslip.error("Invalid popover placement value for hotspot '" + hotspot.name + "'");
 | |
|             break;
 | |
|     }
 | |
| 
 | |
|     // position arrow
 | |
|     arrow_placement = "arrow-" + arrow_placement;
 | |
|     $(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`)
 | |
|         .removeClass("arrow-top arrow-left arrow-bottom arrow-right")
 | |
|         .addClass(arrow_placement);
 | |
| 
 | |
|     // position popover
 | |
|     let popover_placement;
 | |
|     if (orientation === VIEWPORT_CENTER) {
 | |
|         popover_placement = {
 | |
|             top: "45%",
 | |
|             left: "50%",
 | |
|             transform: "translate(-50%, -50%)",
 | |
|         };
 | |
|     } else {
 | |
|         const client_rect = $(hotspot.location.element).get(0).getBoundingClientRect();
 | |
|         popover_placement = {
 | |
|             top: client_rect.top + popover_offset.top,
 | |
|             left: client_rect.left + popover_offset.left,
 | |
|             transform: "",
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     $(`#hotspot_${CSS.escape(hotspot.name)}_overlay .hotspot-popover`).css(popover_placement);
 | |
| }
 | |
| 
 | |
| function insert_hotspot_into_DOM(hotspot) {
 | |
|     const hotspot_overlay_HTML = render_hotspot_overlay({
 | |
|         name: hotspot.name,
 | |
|         title: hotspot.title,
 | |
|         description: hotspot.description,
 | |
|         img: WHALE,
 | |
|     });
 | |
| 
 | |
|     const hotspot_icon_HTML = render_hotspot_icon({
 | |
|         name: hotspot.name,
 | |
|     });
 | |
| 
 | |
|     setTimeout(() => {
 | |
|         $("body").prepend(hotspot_icon_HTML);
 | |
|         $("body").prepend(hotspot_overlay_HTML);
 | |
|         if (place_icon(hotspot)) {
 | |
|             place_popover(hotspot);
 | |
|         }
 | |
| 
 | |
|         // reposition on any event that might update the UI
 | |
|         for (const event_name of ["resize", "scroll", "onkeydown", "click"]) {
 | |
|             window.addEventListener(
 | |
|                 event_name,
 | |
|                 _.debounce(() => {
 | |
|                     if (place_icon(hotspot)) {
 | |
|                         place_popover(hotspot);
 | |
|                     }
 | |
|                 }, 10),
 | |
|                 true,
 | |
|             );
 | |
|         }
 | |
|     }, hotspot.delay * 1000);
 | |
| }
 | |
| 
 | |
| export function is_open() {
 | |
|     return $(".hotspot.overlay").hasClass("show");
 | |
| }
 | |
| 
 | |
| export function close_hotspot_icon(elem) {
 | |
|     $(elem).animate(
 | |
|         {opacity: 0},
 | |
|         {
 | |
|             duration: 300,
 | |
|             done: function () {
 | |
|                 $(elem).css({display: "none"});
 | |
|             }.bind(elem),
 | |
|         },
 | |
|     );
 | |
| }
 | |
| 
 | |
| function close_read_hotspots(new_hotspots) {
 | |
|     const unwanted_hotspots = _.difference(
 | |
|         Array.from(HOTSPOT_LOCATIONS.keys()),
 | |
|         new_hotspots.map((hotspot) => hotspot.name),
 | |
|     );
 | |
| 
 | |
|     for (const hotspot_name of unwanted_hotspots) {
 | |
|         close_hotspot_icon($(`#hotspot_${CSS.escape(hotspot_name)}_icon`));
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function load_new(new_hotspots) {
 | |
|     close_read_hotspots(new_hotspots);
 | |
|     for (const hotspot of new_hotspots) {
 | |
|         hotspot.location = HOTSPOT_LOCATIONS.get(hotspot.name);
 | |
|         insert_hotspot_into_DOM(hotspot);
 | |
|     }
 | |
| }
 | |
| 
 | |
| export function initialize() {
 | |
|     load_new(page_params.hotspots);
 | |
| }
 |