import $ from "jquery"; import panzoom from "panzoom"; import render_lightbox_overlay from "../templates/lightbox_overlay.hbs"; import * as blueslip from "./blueslip"; import * as message_store from "./message_store"; import * as overlays from "./overlays"; import * as people from "./people"; import * as popovers from "./popovers"; import * as rows from "./rows"; let is_open = false; // the asset map is a map of all retrieved images and YouTube videos that are // memoized instead of being looked up multiple times. const asset_map = new Map(); export class PanZoomControl { // Class for both initializing and controlling the // the pan/zoom functionality. constructor(container) { this.container = container; this.panzoom = panzoom(this.container, { smoothScroll: false, // Ideally we'd set `bounds` here, but that feature is // currently broken upstream. See // https://github.com/anvaka/panzoom/issues/112. maxZoom: 100, minZoom: 0.1, filterKey() { // Disable the library's built in keybindings return true; }, }); // The following events are necessary to prevent the click event // firing where the user "unclicks" at the end of the drag, which // was causing accidental overlay closes in some situations. this.panzoom.on("pan", () => { // Marks this overlay as needing to stay open. $("#lightbox_overlay").data("noclose", true); // Enable the panzoom reset button. $("#lightbox_overlay .lightbox-zoom-reset").removeClass("disabled"); }); this.panzoom.on("panend", (e) => { // Check if the image has been panned out of view. this.constrainImage(e); // Don't remove the noclose attribute on this overlay until after paint, // otherwise it will be removed too early and close the lightbox // unintentionally. setTimeout(() => { $("#lightbox_overlay").data("noclose", false); }, 0); }); this.panzoom.on("zoom", (e) => { // Check if the image has been zoomed out of view. // We are using the zoom event instead of zoomend because the zoomend // event does not fire when using the scroll wheel or pinch to zoom. // https://github.com/anvaka/panzoom/issues/250 this.constrainImage(e); // Enable the panzoom reset button. $("#lightbox_overlay .lightbox-zoom-reset").removeClass("disabled"); }); // key bindings document.addEventListener("keydown", (e) => { if (!overlays.lightbox_open()) { return; } switch (e.key) { case "Z": case "+": this.zoomIn(); break; case "z": case "-": this.zoomOut(); break; case "v": overlays.close_overlay("lightbox"); break; } e.preventDefault(); e.stopPropagation(); }); } constrainImage(e) { // Instead of using panzoom's built in bounds option which was buggy // at the time of this writing, we act on pan/zoom events and move the // image back in to view if it is moved beyond the image-preview container. // See https://github.com/anvaka/panzoom/issues/112 for upstream discussion. const {scale, x, y} = e.getTransform(); const image_width = $(".zoom-element > img")[0].clientWidth * scale; const image_height = $(".zoom-element > img")[0].clientHeight * scale; const zoom_element_width = $(".zoom-element")[0].clientWidth * scale; const zoom_element_height = $(".zoom-element")[0].clientHeight * scale; const max_translate_x = $(".image-preview")[0].clientWidth; const max_translate_y = $(".image-preview")[0].clientHeight; // When the image is dragged out of the image-preview container // (max_translate) it will be "snapped" back so that the number // of pixels set below will remain visible in the dimension it was dragged. const return_buffer = 50 * scale; // Move the image if it gets within this many pixels of the edge. const border = 20; const zoom_border_width = (zoom_element_width - image_width) / 2 + image_width; const zoom_border_height = (zoom_element_height - image_height) / 2 + image_height; const modified_x = x + zoom_border_width; const modified_y = y + zoom_border_height; if (modified_x < 0 + border) { // Image has been dragged beyond the LEFT of the view. const move_by = modified_x * -1; e.moveBy(move_by + return_buffer, 0, false); } else if (modified_x - image_width > max_translate_x - border) { // Image has been dragged beyond the RIGHT of the view. const move_by = modified_x - max_translate_x - image_width; e.moveBy(-move_by - return_buffer, 0, false); } if (modified_y < 0 + border) { // Image has been dragged beyond the TOP of the view. const move_by = modified_y * -1; e.moveBy(0, move_by + return_buffer, false); } else if (modified_y - image_height > max_translate_y - border) { // Image has been dragged beyond the BOTTOM of the view. const move_by = modified_y - max_translate_y - image_height; e.moveBy(0, -move_by - return_buffer, false); } } reset() { // To reset the panzoom state, we want to: // Reset zoom to the initial state. this.panzoom.zoomAbs(0, 0, 1); // Re-center the image. this.panzoom.moveTo(0, 0); // Always ensure that the overlay is available for click to close. // This way we don't rely on the above events firing panend, // of which there is some anecdotal evidence that suggests they // might be prone to race conditions. $("#lightbox_overlay").data("noclose", false); // Disable the lightbox reset button to reflect the state that // the image has not been panned or zoomed. $("#lightbox_overlay .lightbox-zoom-reset").addClass("disabled"); } zoomIn() { const w = $(".image-preview").width(); const h = $(".image-preview").height(); this.panzoom.smoothZoom(w / 2, h / 2, 1.5); } zoomOut() { const w = $(".image-preview").width(); const h = $(".image-preview").height(); this.panzoom.smoothZoom(w / 2, h / 2, 0.5); } isActive() { return $(".image-preview .zoom-element img").length > 0; } } export function clear_for_testing() { is_open = false; asset_map.clear(); } export function render_lightbox_list_images(preview_source) { if (!is_open) { const images = Array.prototype.slice.call($(".focused_table .message_inline_image img")); const $image_list = $("#lightbox_overlay .image-list").empty(); for (const img of images) { const src = img.getAttribute("src"); const className = preview_source === src ? "image selected" : "image"; const $node = $("