mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	The last_message CSS class didn't mean what it said it did, due to issues with live update. Further, this logic was poorly written, with `$message` changing types from a .message_row to a .recipient_row for now apparent reason. I was able to reproduce at least one bug where the `v` shortcut would not correctly open the lightbox that is fixed by this rewrite.
		
			
				
	
	
		
			567 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			567 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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 = $("<div>")
 | 
						|
                .addClass(className)
 | 
						|
                .attr("data-src", src)
 | 
						|
                .css({backgroundImage: "url(" + src + ")"});
 | 
						|
 | 
						|
            $image_list.append($node);
 | 
						|
 | 
						|
            // We parse the data for each image to show in the list,
 | 
						|
            // while we still have its original DOM element handy, so
 | 
						|
            // that navigating within the list only needs the `src`
 | 
						|
            // attribute used to construct the node object above.
 | 
						|
            parse_image_data(img);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function display_image(payload) {
 | 
						|
    render_lightbox_list_images(payload.preview);
 | 
						|
 | 
						|
    $(".player-container").hide();
 | 
						|
    $(".image-preview, .image-actions, .image-description, .download, .lightbox-zoom-reset").show();
 | 
						|
 | 
						|
    const $img_container = $("#lightbox_overlay .image-preview > .zoom-element");
 | 
						|
    const img = new Image();
 | 
						|
    img.src = payload.source;
 | 
						|
    $img_container.html(img).show();
 | 
						|
 | 
						|
    const filename = payload.url?.split("/").pop();
 | 
						|
    $(".image-description .title")
 | 
						|
        .text(payload.title || "N/A")
 | 
						|
        .attr("aria-label", payload.title || "N/A")
 | 
						|
        .prop("data-filename", filename || "N/A");
 | 
						|
    $(".image-description .user").text(payload.user).prop("title", payload.user);
 | 
						|
 | 
						|
    $(".image-actions .open").attr("href", payload.source);
 | 
						|
 | 
						|
    const url = new URL(payload.source, window.location.href);
 | 
						|
    const same_origin = url.origin === window.location.origin;
 | 
						|
    if (same_origin && url.pathname.startsWith("/user_uploads/")) {
 | 
						|
        // Switch to the "download" handler, so S3 URLs set their Content-Disposition
 | 
						|
        url.pathname = "/user_uploads/download/" + url.pathname.slice("/user_uploads/".length);
 | 
						|
        $(".image-actions .download").attr("href", url.href);
 | 
						|
    } else if (same_origin) {
 | 
						|
        $(".image-actions .download").attr("href", payload.source);
 | 
						|
    } else {
 | 
						|
        // If it's not same-origin, and we don't know how to tell the remote service to put a
 | 
						|
        // content-disposition on it, the download can't possibly download, just show -- so hide the
 | 
						|
        // element.
 | 
						|
        $(".image-actions .download").hide();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function display_video(payload) {
 | 
						|
    render_lightbox_list_images(payload.preview);
 | 
						|
 | 
						|
    $(
 | 
						|
        "#lightbox_overlay .image-preview, .image-description, .download, .lightbox-zoom-reset",
 | 
						|
    ).hide();
 | 
						|
 | 
						|
    let source;
 | 
						|
    switch (payload.type) {
 | 
						|
        case "youtube-video":
 | 
						|
            source = "https://www.youtube.com/embed/" + payload.source;
 | 
						|
            break;
 | 
						|
        case "vimeo-video":
 | 
						|
            source = "https://player.vimeo.com/video/" + payload.source;
 | 
						|
            break;
 | 
						|
        case "embed-video":
 | 
						|
            // Use data: to load the player in a unique origin for security.
 | 
						|
            source =
 | 
						|
                "data:text/html," +
 | 
						|
                window.encodeURIComponent(
 | 
						|
                    "<!DOCTYPE html><style>iframe{position:absolute;left:0;top:0;width:100%;height:100%;box-sizing:border-box}</style>" +
 | 
						|
                        payload.source,
 | 
						|
                );
 | 
						|
            break;
 | 
						|
    }
 | 
						|
 | 
						|
    const $iframe = $("<iframe>");
 | 
						|
    $iframe.attr(
 | 
						|
        "sandbox",
 | 
						|
        "allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts",
 | 
						|
    );
 | 
						|
    $iframe.attr("src", source);
 | 
						|
    $iframe.attr("frameborder", 0);
 | 
						|
    $iframe.attr("allowfullscreen", true);
 | 
						|
 | 
						|
    $("#lightbox_overlay .player-container").html($iframe).show();
 | 
						|
    $(".image-actions .open").attr("href", payload.url);
 | 
						|
}
 | 
						|
 | 
						|
export function build_open_image_function(on_close) {
 | 
						|
    if (on_close === undefined) {
 | 
						|
        on_close = function () {
 | 
						|
            $(".player-container iframe").remove();
 | 
						|
            is_open = false;
 | 
						|
            document.activeElement.blur();
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    return function ($image) {
 | 
						|
        // if the asset_map already contains the metadata required to display the
 | 
						|
        // asset, just recall that metadata.
 | 
						|
        let $preview_src = $image.attr("src");
 | 
						|
        let payload = asset_map.get($preview_src);
 | 
						|
        if (payload === undefined) {
 | 
						|
            if ($preview_src.endsWith("&size=full")) {
 | 
						|
                // while fetching an image for canvas, `src` attribute supplies
 | 
						|
                // full-sized image instead of thumbnail, so we have to replace
 | 
						|
                // `size=full` with `size=thumbnail`.
 | 
						|
                //
 | 
						|
                // TODO: This is a hack to work around the fact that for
 | 
						|
                // the lightbox canvas, the `src` is the data-fullsize-src
 | 
						|
                // for the image, not the original thumbnail used to open
 | 
						|
                // the lightbox.  A better fix will be to check a
 | 
						|
                // `data-thumbnail-src` attribute that we add to the
 | 
						|
                // canvas elements.
 | 
						|
                $preview_src = $preview_src.slice(0, -"full".length) + "thumbnail";
 | 
						|
                payload = asset_map.get($preview_src);
 | 
						|
            }
 | 
						|
            if (payload === undefined) {
 | 
						|
                payload = parse_image_data($image);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (payload.type.match("-video")) {
 | 
						|
            display_video(payload);
 | 
						|
        } else if (payload.type === "image") {
 | 
						|
            display_image(payload);
 | 
						|
        }
 | 
						|
 | 
						|
        if (is_open) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        overlays.open_overlay({
 | 
						|
            name: "lightbox",
 | 
						|
            $overlay: $("#lightbox_overlay"),
 | 
						|
            on_close,
 | 
						|
        });
 | 
						|
 | 
						|
        popovers.hide_all();
 | 
						|
        is_open = true;
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
export function show_from_selected_message() {
 | 
						|
    const $message_selected = $(".selected_message");
 | 
						|
    let $message = $message_selected;
 | 
						|
    let $image = $message.find(".message_inline_image img");
 | 
						|
    let $prev_traverse = false;
 | 
						|
 | 
						|
    // First, we walk upwards/backwards, starting with the current
 | 
						|
    // message, looking for an image to preview.
 | 
						|
    //
 | 
						|
    // Care must be taken, since both recipient_row elements and
 | 
						|
    // message_row objects have siblings of different types, such as
 | 
						|
    // date elements.
 | 
						|
    while ($image.length === 0) {
 | 
						|
        if ($message.prev().length === 0) {
 | 
						|
            const $prev_message_group = $message.parent().prevAll(".recipient_row").first();
 | 
						|
            if ($prev_message_group.length === 0) {
 | 
						|
                $prev_traverse = true;
 | 
						|
                $message = $message_selected;
 | 
						|
                break;
 | 
						|
            } else {
 | 
						|
                $message = $prev_message_group.find(".message_row").last();
 | 
						|
                $image = $message.find(".message_inline_image img");
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        $message = $message.prev();
 | 
						|
        $image = $message.find(".message_inline_image img");
 | 
						|
    }
 | 
						|
 | 
						|
    if ($prev_traverse) {
 | 
						|
        while ($image.length === 0) {
 | 
						|
            if ($message.next().length === 0) {
 | 
						|
                const $next_message_group = $message.parent().nextAll(".recipient_row").first();
 | 
						|
                if ($next_message_group.length === 0) {
 | 
						|
                    break;
 | 
						|
                } else {
 | 
						|
                    $message = $next_message_group.find(".message_row").first();
 | 
						|
                    $image = $message.find(".message_inline_image img");
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            $message = $message.next();
 | 
						|
            $image = $message.find(".message_inline_image img");
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if ($image.length !== 0) {
 | 
						|
        const open_image = build_open_image_function();
 | 
						|
        open_image($image);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// retrieve the metadata from the DOM and store into the asset_map.
 | 
						|
export function parse_image_data(image) {
 | 
						|
    const $image = $(image);
 | 
						|
    const preview_src = $image.attr("src");
 | 
						|
 | 
						|
    if (asset_map.has(preview_src)) {
 | 
						|
        // check if image's data is already present in asset_map.
 | 
						|
        return asset_map.get(preview_src);
 | 
						|
    }
 | 
						|
 | 
						|
    // if wrapped in the .youtube-video class, it will be length = 1, and therefore
 | 
						|
    // cast to true.
 | 
						|
    const is_youtube_video = Boolean($image.closest(".youtube-video").length);
 | 
						|
    const is_vimeo_video = Boolean($image.closest(".vimeo-video").length);
 | 
						|
    const is_embed_video = Boolean($image.closest(".embed-video").length);
 | 
						|
 | 
						|
    // check if image is descendent of #compose .preview_content
 | 
						|
    const is_compose_preview_image = $image.closest("#compose .preview_content").length === 1;
 | 
						|
 | 
						|
    const $parent = $image.parent();
 | 
						|
    let type;
 | 
						|
    let source;
 | 
						|
    const url = $parent.attr("href");
 | 
						|
    if (is_youtube_video) {
 | 
						|
        type = "youtube-video";
 | 
						|
        source = $parent.attr("data-id");
 | 
						|
    } else if (is_vimeo_video) {
 | 
						|
        type = "vimeo-video";
 | 
						|
        source = $parent.attr("data-id");
 | 
						|
    } else if (is_embed_video) {
 | 
						|
        type = "embed-video";
 | 
						|
        source = $parent.attr("data-id");
 | 
						|
    } else {
 | 
						|
        type = "image";
 | 
						|
        if ($image.attr("data-src-fullsize")) {
 | 
						|
            source = $image.attr("data-src-fullsize");
 | 
						|
        } else {
 | 
						|
            source = preview_src;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    let sender_full_name;
 | 
						|
    if (is_compose_preview_image) {
 | 
						|
        sender_full_name = people.my_full_name();
 | 
						|
    } else {
 | 
						|
        const $message = $parent.closest("[zid]");
 | 
						|
        const zid = rows.id($message);
 | 
						|
        const message = message_store.get(zid);
 | 
						|
        if (message === undefined) {
 | 
						|
            blueslip.error("Lightbox for unknown message " + zid);
 | 
						|
        } else {
 | 
						|
            sender_full_name = message.sender_full_name;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    const payload = {
 | 
						|
        user: sender_full_name,
 | 
						|
        title: $parent.attr("aria-label") || $parent.attr("href"),
 | 
						|
        type,
 | 
						|
        preview: preview_src,
 | 
						|
        source,
 | 
						|
        url,
 | 
						|
    };
 | 
						|
 | 
						|
    asset_map.set(preview_src, payload);
 | 
						|
    return payload;
 | 
						|
}
 | 
						|
 | 
						|
export function prev() {
 | 
						|
    $(".image-list .image.selected").prev().trigger("click");
 | 
						|
}
 | 
						|
 | 
						|
export function next() {
 | 
						|
    $(".image-list .image.selected").next().trigger("click");
 | 
						|
}
 | 
						|
 | 
						|
// this is a block of events that are required for the lightbox to work.
 | 
						|
export function initialize() {
 | 
						|
    // Renders the DOM for the lightbox.
 | 
						|
    const rendered_lightbox_overlay = render_lightbox_overlay();
 | 
						|
    $("body").append(rendered_lightbox_overlay);
 | 
						|
 | 
						|
    // Bind the pan/zoom control the newly created element.
 | 
						|
    const pan_zoom_control = new PanZoomControl(
 | 
						|
        $("#lightbox_overlay .image-preview > .zoom-element")[0],
 | 
						|
    );
 | 
						|
 | 
						|
    const reset_lightbox_state = function () {
 | 
						|
        $(".player-container iframe").remove();
 | 
						|
        is_open = false;
 | 
						|
        document.activeElement.blur();
 | 
						|
        if (pan_zoom_control.isActive()) {
 | 
						|
            pan_zoom_control.reset();
 | 
						|
        }
 | 
						|
    };
 | 
						|
 | 
						|
    const open_image = build_open_image_function(reset_lightbox_state);
 | 
						|
 | 
						|
    $("#main_div, #compose .preview_content").on("click", ".message_inline_image a", function (e) {
 | 
						|
        // prevent the link from opening in a new page.
 | 
						|
        e.preventDefault();
 | 
						|
        // prevent the message compose dialog from happening.
 | 
						|
        e.stopPropagation();
 | 
						|
        const $img = $(this).find("img");
 | 
						|
        open_image($img);
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay .download").on("click", function () {
 | 
						|
        this.blur();
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay").on("click", ".image-list .image", function () {
 | 
						|
        const $image_list = $(this).parent();
 | 
						|
        const $original_image = $(
 | 
						|
            `.message_row img[src='${CSS.escape($(this).attr("data-src"))}']`,
 | 
						|
        );
 | 
						|
 | 
						|
        open_image($original_image);
 | 
						|
 | 
						|
        $(".image-list .image.selected").removeClass("selected");
 | 
						|
        $(this).addClass("selected");
 | 
						|
        pan_zoom_control.reset();
 | 
						|
 | 
						|
        const parentOffset = this.parentNode.clientWidth + this.parentNode.scrollLeft;
 | 
						|
        // this is the left and right of the image compared to its parent.
 | 
						|
        const coords = {
 | 
						|
            left: this.offsetLeft,
 | 
						|
            right: this.offsetLeft + this.clientWidth,
 | 
						|
        };
 | 
						|
 | 
						|
        if (coords.right > parentOffset) {
 | 
						|
            // add 2px margin
 | 
						|
            $image_list.animate(
 | 
						|
                {
 | 
						|
                    scrollLeft: coords.right - this.parentNode.clientWidth + 2,
 | 
						|
                },
 | 
						|
                100,
 | 
						|
            );
 | 
						|
        } else if (coords.left < this.parentNode.scrollLeft) {
 | 
						|
            // subtract 2px margin
 | 
						|
            $image_list.animate({scrollLeft: coords.left - 2}, 100);
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay").on("click", ".center .arrow", function () {
 | 
						|
        const direction = $(this).attr("data-direction");
 | 
						|
 | 
						|
        if (direction === "next") {
 | 
						|
            next();
 | 
						|
        } else if (direction === "prev") {
 | 
						|
            prev();
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay").on("click", ".lightbox-zoom-reset", () => {
 | 
						|
        if (!$("#lightbox_overlay .lightbox-zoom-reset").hasClass("disabled")) {
 | 
						|
            const $img = $("#lightbox_overlay").find(".image-preview img");
 | 
						|
            open_image($img);
 | 
						|
            pan_zoom_control.reset();
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay .player-container").on("click", function () {
 | 
						|
        if ($(this).is(".player-container")) {
 | 
						|
            reset_lightbox_state();
 | 
						|
            overlays.close_active();
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay").on("click", ".image-info-wrapper, .center", (e) => {
 | 
						|
        if ($(e.target).is(".image-info-wrapper, .center")) {
 | 
						|
            reset_lightbox_state();
 | 
						|
            overlays.close_overlay("lightbox");
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    $("#lightbox_overlay .image-preview").on("click", (e) => {
 | 
						|
        // Ensure that the click isn't on the image itself, and that
 | 
						|
        // the window isn't marked as disabled to click to close.
 | 
						|
        if (!$(e.target).is("img") && !$("#lightbox_overlay").data("noclose")) {
 | 
						|
            reset_lightbox_state();
 | 
						|
            overlays.close_overlay("lightbox");
 | 
						|
        }
 | 
						|
    });
 | 
						|
}
 |