mirror of
https://github.com/zulip/zulip.git
synced 2025-10-28 02:23:57 +00:00
ES and TypeScript modules are strict by default and don’t need this directive. ESLint will remind us to add it to new CommonJS files and remove it from ES and TypeScript modules. Signed-off-by: Anders Kaseorg <anders@zulip.com>
298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const funcs = {
|
|
setZoom(meta, zoom) {
|
|
// condition to handle zooming event by zoom hotkeys
|
|
if (zoom === "+") {
|
|
zoom = meta.zoom * 1.2;
|
|
} else if (zoom === "-") {
|
|
zoom = meta.zoom / 1.2;
|
|
}
|
|
// make sure the zoom is above 1 and below the maxZoom.
|
|
meta.zoom = Math.min(Math.max(zoom, 1), meta.maxZoom);
|
|
},
|
|
|
|
// this is a function given a canvas that attaches all of the events
|
|
// required to pan and zoom.
|
|
attachEvents(canvas, context, meta) {
|
|
let mousedown = false;
|
|
|
|
// wheelEvent.deltaMode is a value that describes what the unit is
|
|
// for the `deltaX`, `deltaY`, and `deltaZ` properties.
|
|
const DELTA_MODE = {
|
|
PIXEL: 0,
|
|
LINE: 1,
|
|
PAGE: 2,
|
|
};
|
|
|
|
// give object structure in `mousedown`, because its props are only
|
|
// ever set once `mousedown` + `mousemove` is triggered.
|
|
let lastPosition = {};
|
|
|
|
// in browsers such as Safari, the `e.movementX` and `e.movementY`
|
|
// props don't exist, so we need to create them as a difference of
|
|
// where the last `layerX` and `layerY` movements since the last
|
|
// `mousemove` event in this `mousedown` event were registered.
|
|
const polyfillMouseMovement = (e) => {
|
|
e.movementX = e.layerX - lastPosition.x || 0;
|
|
e.movementY = e.layerY - lastPosition.y || 0;
|
|
|
|
lastPosition = {
|
|
x: e.layerX,
|
|
y: e.layerY,
|
|
};
|
|
};
|
|
|
|
// use the wheel event rather than scroll because this isn't
|
|
// actually an element that can scroll. The wheel event will
|
|
// detect the *gesture* of scrolling over an element, without actually
|
|
// worrying about scrollable content.
|
|
canvas.addEventListener("wheel", (e) => {
|
|
e.preventDefault();
|
|
|
|
// this is to reverse scrolling directions for the image.
|
|
let delta = meta.direction * e.deltaY;
|
|
|
|
if (e.deltaMode === DELTA_MODE.LINE) {
|
|
// the vertical height in pixels of an approximate line.
|
|
delta *= 15;
|
|
}
|
|
|
|
if (e.deltaMode === DELTA_MODE.PAGE) {
|
|
// the vertical height in pixels of an approximate page.
|
|
delta *= 300;
|
|
}
|
|
|
|
// this is calculated as the user defined speed times the normalizer
|
|
// (which just is what it takes to take the raw delta and transform
|
|
// it to a normal speed), multiply it against the current zoom.
|
|
// Example:
|
|
// delta = 8
|
|
// normalizedDelta = delta * (1 / 20) * 1 = 0.4
|
|
// zoom = zoom * (0.4 / 100) + 1
|
|
const zoom =
|
|
meta.zoom * ((meta.speed * meta.internalSpeedMultiplier * delta) / 100 + 1);
|
|
|
|
funcs.setZoom(meta, zoom);
|
|
funcs.displayImage(canvas, context, meta);
|
|
|
|
return false;
|
|
});
|
|
|
|
// the only valid mousedown events should originate inside of the
|
|
// canvas.
|
|
canvas.addEventListener("mousedown", () => {
|
|
mousedown = true;
|
|
});
|
|
|
|
// on mousemove, actually run the pan events.
|
|
canvas.addEventListener("mousemove", (e) => {
|
|
// to pan, there must be mousedown and mousemove, check if valid.
|
|
if (mousedown === true) {
|
|
polyfillMouseMovement(e);
|
|
// find the percent of movement relative to the canvas width
|
|
// since e.movementX, e.movementY are in px.
|
|
const percentMovement = {
|
|
x: e.movementX / canvas.width,
|
|
y: e.movementY / canvas.height,
|
|
};
|
|
|
|
// add the percentMovement to the meta coordinates but divide
|
|
// out by the zoom ratio because when zoomed in 10x for example
|
|
// moving the photo by 1% will appear like 10% on the <canvas>.
|
|
meta.coords.x += (percentMovement.x * 2) / meta.zoom;
|
|
meta.coords.y += (percentMovement.y * 2) / meta.zoom;
|
|
|
|
// redraw the image.
|
|
funcs.displayImage(canvas, context, meta);
|
|
}
|
|
});
|
|
|
|
// event listener to handle zoom in and out from using keyboard keys z/Z and +/-
|
|
// in the canvas
|
|
// these hotkeys are not implemented in static/js/hotkey.js as the code in
|
|
// static/js/lightbox_canvas.js and static/js/lightbox.js isn't written a way
|
|
// that the LightboxCanvas instance created in lightbox.js can be
|
|
// accessed from hotkey.js. Major code refactoring is required in lightbox.js
|
|
// to implement these keyboard shortcuts in hotkey.js
|
|
document.addEventListener("keydown", (e) => {
|
|
if (!overlays.lightbox_open()) {
|
|
return;
|
|
}
|
|
if (e.key === "Z" || e.key === "+") {
|
|
funcs.setZoom(meta, "+");
|
|
funcs.displayImage(canvas, context, meta);
|
|
} else if (e.key === "z" || e.key === "-") {
|
|
funcs.setZoom(meta, "-");
|
|
funcs.displayImage(canvas, context, meta);
|
|
} else if (e.key === "v") {
|
|
overlays.close_overlay("lightbox");
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
|
|
// make sure that when the mousedown is lifted on <canvas>to prevent
|
|
// panning events.
|
|
canvas.addEventListener("mouseup", () => {
|
|
mousedown = false;
|
|
// reset this to be empty so that the values will `NaN` on first
|
|
// mousemove and default to a change of (0, 0).
|
|
lastPosition = {};
|
|
});
|
|
|
|
// do so on the document.body as well, though depending on the infra,
|
|
// these are less reliable as preventDefault may prevent these events
|
|
// from propagating all the way to the <body>.
|
|
document.body.addEventListener("mouseup", function body_mouseup() {
|
|
if (document.body.contains(canvas)) {
|
|
mousedown = false;
|
|
} else {
|
|
document.body.removeEventListener("mouseup", body_mouseup);
|
|
}
|
|
});
|
|
|
|
window.addEventListener("resize", function window_resize() {
|
|
if (document.body.contains(canvas)) {
|
|
funcs.sizeCanvas(canvas, meta);
|
|
funcs.displayImage(canvas, context, meta);
|
|
} else {
|
|
window.removeEventListener("resize", window_resize);
|
|
}
|
|
});
|
|
},
|
|
|
|
imageRatio(image) {
|
|
return image.naturalWidth / image.naturalHeight;
|
|
},
|
|
|
|
displayImage(canvas, context, meta) {
|
|
meta.coords.x = Math.max(1 / (meta.zoom * 2), meta.coords.x);
|
|
meta.coords.x = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.x);
|
|
|
|
meta.coords.y = Math.max(1 / (meta.zoom * 2), meta.coords.y);
|
|
meta.coords.y = Math.min(1 - 1 / (meta.zoom * 2), meta.coords.y);
|
|
|
|
const c = {
|
|
x: meta.coords.x - 1,
|
|
y: meta.coords.y - 1,
|
|
};
|
|
|
|
const x = meta.zoom * c.x * canvas.width + canvas.width / 2;
|
|
const y = meta.zoom * c.y * canvas.height + canvas.height / 2;
|
|
const w = canvas.width * meta.zoom;
|
|
const h = canvas.height * meta.zoom;
|
|
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
context.imageSmoothingEnabled = false;
|
|
|
|
context.drawImage(meta.image, x, y, w, h);
|
|
},
|
|
|
|
// the `sizeCanvas` method figures out the appropriate bounding box for
|
|
// the canvas given a parent that has constraints.
|
|
// for example, if a photo has a ration of 1.5:1 (w:h), and the parent
|
|
// box is 1:1 respectively, we want to stretch the photo to be as large
|
|
// as we can, which means that we check if having the photo width = 100%
|
|
// means that the height is less than 100% of the parent height. If so,
|
|
// then we size the photo as w = 100%, h = 100% / 1.5.
|
|
sizeCanvas(canvas, meta) {
|
|
if (canvas.parentNode === null) {
|
|
return;
|
|
}
|
|
|
|
if (typeof meta.onresize === "function") {
|
|
meta.onresize(canvas);
|
|
}
|
|
|
|
const parent = {
|
|
width: canvas.parentNode.clientWidth,
|
|
height: canvas.parentNode.clientHeight,
|
|
};
|
|
|
|
if (parent.height * meta.ratio > parent.width) {
|
|
canvas.width = parent.width * 2;
|
|
canvas.style.width = parent.width + "px";
|
|
|
|
canvas.height = (parent.width / meta.ratio) * 2;
|
|
canvas.style.height = parent.width / meta.ratio + "px";
|
|
} else {
|
|
canvas.height = parent.height * 2;
|
|
canvas.style.height = parent.height + "px";
|
|
|
|
canvas.width = parent.height * meta.ratio * 2;
|
|
canvas.style.width = parent.height * meta.ratio + "px";
|
|
}
|
|
|
|
blueslip.warn("Please specify a 'data-width' or 'data-height' argument for canvas.");
|
|
},
|
|
};
|
|
|
|
class LightboxCanvas {
|
|
meta = {
|
|
direction: -1,
|
|
zoom: 1,
|
|
image: null,
|
|
coords: {
|
|
x: 0.5,
|
|
y: 0.5,
|
|
},
|
|
speed: 1,
|
|
// this is to normalize the speed to what I would consider to be
|
|
// "standard" zoom speed.
|
|
internalSpeedMultiplier: 0.05,
|
|
maxZoom: 10,
|
|
};
|
|
|
|
constructor(el) {
|
|
if (el instanceof Node) {
|
|
this.canvas = el;
|
|
} else if (typeof el === "string") {
|
|
this.canvas = document.querySelector(el);
|
|
} else {
|
|
throw new TypeError("'LightboxCanvas' accepts either string selector or node.");
|
|
}
|
|
|
|
this.context = this.canvas.getContext("2d");
|
|
|
|
this.meta.image = new Image();
|
|
this.meta.image.src = this.canvas.getAttribute("data-src");
|
|
this.meta.image.addEventListener("load", () => {
|
|
this.meta.ratio = funcs.imageRatio(this.meta.image);
|
|
|
|
funcs.sizeCanvas(this.canvas, this.meta);
|
|
funcs.displayImage(this.canvas, this.context, this.meta);
|
|
});
|
|
|
|
this.canvas.image = this.meta.image;
|
|
|
|
funcs.attachEvents(this.canvas, this.context, this.meta);
|
|
}
|
|
|
|
// set the speed at which scrolling zooms in on a photo.
|
|
speed(speed) {
|
|
this.meta.speed = speed;
|
|
}
|
|
|
|
// set the max zoom of the `LightboxCanvas` canvas as a mult of the total width.
|
|
maxZoom(maxZoom) {
|
|
this.meta.maxZoom = maxZoom;
|
|
}
|
|
|
|
reverseScrollDirection() {
|
|
this.meta.direction = 1;
|
|
}
|
|
|
|
setZoom(zoom) {
|
|
funcs.setZoom(this.meta, zoom);
|
|
funcs.displayImage(this.canvas, this.context, this.meta);
|
|
}
|
|
|
|
resize(callback) {
|
|
this.meta.onresize = callback;
|
|
}
|
|
}
|
|
|
|
module.exports = LightboxCanvas;
|
|
window.LightboxCanvas = LightboxCanvas;
|