feat: Full width images (#4389)
* feat: Full width images * lint * fix: Enable TOC overlaid on full size images * Vendorize react-medium-image-zoom * tsc * fix * Remove body scroll lock
This commit is contained in:
555
shared/editor/components/ImageZoom/utils.ts
Normal file
555
shared/editor/components/ImageZoom/utils.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import { CSSProperties } from "react";
|
||||
import type { SupportedImage } from "./types";
|
||||
|
||||
interface TestElType {
|
||||
(type: string, el: unknown): boolean;
|
||||
}
|
||||
|
||||
const testElType: TestElType = (type, el) =>
|
||||
type === (el as Element)?.tagName?.toUpperCase?.();
|
||||
|
||||
export const testDiv = (el: unknown): el is HTMLDivElement | HTMLSpanElement =>
|
||||
testElType("DIV", el) || testElType("SPAN", el);
|
||||
|
||||
export const testImg = (el: unknown): el is HTMLImageElement =>
|
||||
testElType("IMG", el);
|
||||
|
||||
export const testSvg = (el: unknown): el is SVGElement => testElType("SVG", el);
|
||||
|
||||
export interface GetScaleToWindow {
|
||||
(data: { width: number; height: number; offset: number }): number;
|
||||
}
|
||||
|
||||
export const getScaleToWindow: GetScaleToWindow = ({
|
||||
height,
|
||||
offset,
|
||||
width,
|
||||
}) => {
|
||||
return Math.min(
|
||||
(window.innerWidth - offset * 2) / width, // scale X-axis
|
||||
(window.innerHeight - offset * 2) / height // scale Y-axis
|
||||
);
|
||||
};
|
||||
|
||||
export interface GetScaleToWindowMax {
|
||||
(data: {
|
||||
containerHeight: number;
|
||||
containerWidth: number;
|
||||
offset: number;
|
||||
targetHeight: number;
|
||||
targetWidth: number;
|
||||
}): number;
|
||||
}
|
||||
|
||||
export const getScaleToWindowMax: GetScaleToWindowMax = ({
|
||||
containerHeight,
|
||||
containerWidth,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
}) => {
|
||||
const scale = getScaleToWindow({
|
||||
height: targetHeight,
|
||||
offset,
|
||||
width: targetWidth,
|
||||
});
|
||||
|
||||
const ratio =
|
||||
targetWidth > targetHeight
|
||||
? targetWidth / containerWidth
|
||||
: targetHeight / containerHeight;
|
||||
|
||||
return scale > 1 ? ratio : scale * ratio;
|
||||
};
|
||||
|
||||
export interface GetScale {
|
||||
(data: {
|
||||
containerHeight: number;
|
||||
containerWidth: number;
|
||||
hasScalableSrc: boolean;
|
||||
offset: number;
|
||||
targetHeight: number;
|
||||
targetWidth: number;
|
||||
}): number;
|
||||
}
|
||||
|
||||
export const getScale: GetScale = ({
|
||||
containerHeight,
|
||||
containerWidth,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
}) => {
|
||||
return !hasScalableSrc && targetHeight && targetWidth
|
||||
? getScaleToWindowMax({
|
||||
containerHeight,
|
||||
containerWidth,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
})
|
||||
: getScaleToWindow({
|
||||
height: containerHeight,
|
||||
offset,
|
||||
width: containerWidth,
|
||||
});
|
||||
};
|
||||
|
||||
const URL_REGEX = /url(?:\(['"]?)(.*?)(?:['"]?\))/;
|
||||
|
||||
export interface GetImgSrc {
|
||||
(imgEl: SupportedImage | null): string | undefined;
|
||||
}
|
||||
|
||||
export const getImgSrc: GetImgSrc = (imgEl) => {
|
||||
if (imgEl) {
|
||||
if (testImg(imgEl)) {
|
||||
return imgEl.currentSrc;
|
||||
} else if (testDiv(imgEl)) {
|
||||
const bgImg = window.getComputedStyle(imgEl).backgroundImage;
|
||||
|
||||
if (bgImg) {
|
||||
return URL_REGEX.exec(bgImg)?.[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
export interface GetImgAlt {
|
||||
(imgEl: SupportedImage | null): string | undefined;
|
||||
}
|
||||
|
||||
export const getImgAlt: GetImgAlt = (imgEl) => {
|
||||
if (imgEl) {
|
||||
if (testImg(imgEl)) {
|
||||
return imgEl.alt ?? undefined;
|
||||
} else {
|
||||
return imgEl.getAttribute("aria-label") ?? undefined;
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
export interface GetImgRegularStyle {
|
||||
(data: {
|
||||
containerHeight: number;
|
||||
containerLeft: number;
|
||||
containerTop: number;
|
||||
containerWidth: number;
|
||||
hasScalableSrc: boolean;
|
||||
offset: number;
|
||||
targetHeight: number;
|
||||
targetWidth: number;
|
||||
}): CSSProperties;
|
||||
}
|
||||
|
||||
export const getImgRegularStyle: GetImgRegularStyle = ({
|
||||
containerHeight,
|
||||
containerLeft,
|
||||
containerTop,
|
||||
containerWidth,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
}) => {
|
||||
const scale = getScale({
|
||||
containerHeight,
|
||||
containerWidth,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
top: containerTop,
|
||||
left: containerLeft,
|
||||
width: containerWidth * scale,
|
||||
height: containerHeight * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
};
|
||||
|
||||
export interface ParsePosition {
|
||||
(data: { position: string; relativeNum: number }): number;
|
||||
}
|
||||
|
||||
export const parsePosition: ParsePosition = ({ position, relativeNum }) => {
|
||||
const positionNum = parseFloat(position);
|
||||
|
||||
return position.endsWith("%")
|
||||
? (relativeNum * positionNum) / 100
|
||||
: positionNum;
|
||||
};
|
||||
|
||||
export interface GetImgObjectFitStyle {
|
||||
(data: {
|
||||
containerHeight: number;
|
||||
containerLeft: number;
|
||||
containerTop: number;
|
||||
containerWidth: number;
|
||||
hasScalableSrc: boolean;
|
||||
objectFit: string;
|
||||
objectPosition: string;
|
||||
offset: number;
|
||||
targetHeight: number;
|
||||
targetWidth: number;
|
||||
}): CSSProperties;
|
||||
}
|
||||
|
||||
export const getImgObjectFitStyle: GetImgObjectFitStyle = ({
|
||||
containerHeight,
|
||||
containerLeft,
|
||||
containerTop,
|
||||
containerWidth,
|
||||
hasScalableSrc,
|
||||
objectFit,
|
||||
objectPosition,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
}) => {
|
||||
if (objectFit === "scale-down") {
|
||||
if (targetWidth <= containerWidth && targetHeight <= containerHeight) {
|
||||
objectFit = "none";
|
||||
} else {
|
||||
objectFit = "contain";
|
||||
}
|
||||
}
|
||||
|
||||
if (objectFit === "cover" || objectFit === "contain") {
|
||||
const widthRatio = containerWidth / targetWidth;
|
||||
const heightRatio = containerHeight / targetHeight;
|
||||
|
||||
const ratio =
|
||||
objectFit === "cover"
|
||||
? Math.max(widthRatio, heightRatio)
|
||||
: Math.min(widthRatio, heightRatio);
|
||||
|
||||
const [posLeft = "50%", posTop = "50%"] = objectPosition.split(" ");
|
||||
const posX = parsePosition({
|
||||
position: posLeft,
|
||||
relativeNum: containerWidth - targetWidth * ratio,
|
||||
});
|
||||
const posY = parsePosition({
|
||||
position: posTop,
|
||||
relativeNum: containerHeight - targetHeight * ratio,
|
||||
});
|
||||
|
||||
const scale = getScale({
|
||||
containerHeight: targetHeight * ratio,
|
||||
containerWidth: targetWidth * ratio,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
top: containerTop + posY,
|
||||
left: containerLeft + posX,
|
||||
width: targetWidth * ratio * scale,
|
||||
height: targetHeight * ratio * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
} else if (objectFit === "none") {
|
||||
const [posLeft = "50%", posTop = "50%"] = objectPosition.split(" ");
|
||||
const posX = parsePosition({
|
||||
position: posLeft,
|
||||
relativeNum: containerWidth - targetWidth,
|
||||
});
|
||||
const posY = parsePosition({
|
||||
position: posTop,
|
||||
relativeNum: containerHeight - targetHeight,
|
||||
});
|
||||
|
||||
const scale = getScale({
|
||||
containerHeight: targetHeight,
|
||||
containerWidth: targetWidth,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
top: containerTop + posY,
|
||||
left: containerLeft + posX,
|
||||
width: targetWidth * scale,
|
||||
height: targetHeight * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
} else if (objectFit === "fill") {
|
||||
const widthRatio = containerWidth / targetWidth;
|
||||
const heightRatio = containerHeight / targetHeight;
|
||||
const ratio = Math.max(widthRatio, heightRatio);
|
||||
|
||||
const scale = getScale({
|
||||
containerHeight: targetHeight * ratio,
|
||||
containerWidth: targetWidth * ratio,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
width: containerWidth * scale,
|
||||
height: containerHeight * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export interface GetDivImgStyle {
|
||||
(data: {
|
||||
backgroundPosition: string;
|
||||
backgroundSize: string;
|
||||
containerHeight: number;
|
||||
containerLeft: number;
|
||||
containerTop: number;
|
||||
containerWidth: number;
|
||||
hasScalableSrc: boolean;
|
||||
offset: number;
|
||||
targetHeight: number;
|
||||
targetWidth: number;
|
||||
}): CSSProperties;
|
||||
}
|
||||
|
||||
export const getDivImgStyle: GetDivImgStyle = ({
|
||||
backgroundPosition,
|
||||
backgroundSize,
|
||||
containerHeight,
|
||||
containerLeft,
|
||||
containerTop,
|
||||
containerWidth,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
}) => {
|
||||
if (backgroundSize === "cover" || backgroundSize === "contain") {
|
||||
const widthRatio = containerWidth / targetWidth;
|
||||
const heightRatio = containerHeight / targetHeight;
|
||||
|
||||
const ratio =
|
||||
backgroundSize === "cover"
|
||||
? Math.max(widthRatio, heightRatio)
|
||||
: Math.min(widthRatio, heightRatio);
|
||||
|
||||
const [posLeft = "50%", posTop = "50%"] = backgroundPosition.split(" ");
|
||||
const posX = parsePosition({
|
||||
position: posLeft,
|
||||
relativeNum: containerWidth - targetWidth * ratio,
|
||||
});
|
||||
const posY = parsePosition({
|
||||
position: posTop,
|
||||
relativeNum: containerHeight - targetHeight * ratio,
|
||||
});
|
||||
|
||||
const scale = getScale({
|
||||
containerHeight: targetHeight * ratio,
|
||||
containerWidth: targetWidth * ratio,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
top: containerTop + posY,
|
||||
left: containerLeft + posX,
|
||||
width: targetWidth * ratio * scale,
|
||||
height: targetHeight * ratio * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
} else if (backgroundSize === "auto") {
|
||||
const [posLeft = "50%", posTop = "50%"] = backgroundPosition.split(" ");
|
||||
const posX = parsePosition({
|
||||
position: posLeft,
|
||||
relativeNum: containerWidth - targetWidth,
|
||||
});
|
||||
const posY = parsePosition({
|
||||
position: posTop,
|
||||
relativeNum: containerHeight - targetHeight,
|
||||
});
|
||||
|
||||
const scale = getScale({
|
||||
containerHeight: targetHeight,
|
||||
containerWidth: targetWidth,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
top: containerTop + posY,
|
||||
left: containerLeft + posX,
|
||||
width: targetWidth * scale,
|
||||
height: targetHeight * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
} else {
|
||||
const [sizeW = "50%", sizeH = "50%"] = backgroundSize.split(" ");
|
||||
const sizeWidth = parsePosition({
|
||||
position: sizeW,
|
||||
relativeNum: containerWidth,
|
||||
});
|
||||
const sizeHeight = parsePosition({
|
||||
position: sizeH,
|
||||
relativeNum: containerHeight,
|
||||
});
|
||||
|
||||
const widthRatio = sizeWidth / targetWidth;
|
||||
const heightRatio = sizeHeight / targetHeight;
|
||||
|
||||
// @TODO: something funny is happening with this ratio
|
||||
const ratio = Math.min(widthRatio, heightRatio);
|
||||
|
||||
const [posLeft = "50%", posTop = "50%"] = backgroundPosition.split(" ");
|
||||
const posX = parsePosition({
|
||||
position: posLeft,
|
||||
relativeNum: containerWidth - targetWidth * ratio,
|
||||
});
|
||||
const posY = parsePosition({
|
||||
position: posTop,
|
||||
relativeNum: containerHeight - targetHeight * ratio,
|
||||
});
|
||||
|
||||
const scale = getScale({
|
||||
containerHeight: targetHeight * ratio,
|
||||
containerWidth: targetWidth * ratio,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight,
|
||||
targetWidth,
|
||||
});
|
||||
|
||||
return {
|
||||
top: containerTop + posY,
|
||||
left: containerLeft + posX,
|
||||
width: targetWidth * ratio * scale,
|
||||
height: targetHeight * ratio * scale,
|
||||
transform: `translate(0,0) scale(${1 / scale})`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const SRC_SVG_REGEX = /\.svg$/i;
|
||||
|
||||
export interface GetStyleModalImg {
|
||||
(data: {
|
||||
hasZoomImg: boolean;
|
||||
imgSrc: string | undefined;
|
||||
isSvg: boolean;
|
||||
isZoomed: boolean;
|
||||
loadedImgEl: HTMLImageElement | undefined;
|
||||
offset: number;
|
||||
shouldRefresh: boolean;
|
||||
targetEl: SupportedImage;
|
||||
}): CSSProperties;
|
||||
}
|
||||
|
||||
export const getStyleModalImg: GetStyleModalImg = ({
|
||||
hasZoomImg,
|
||||
imgSrc,
|
||||
isSvg,
|
||||
isZoomed,
|
||||
loadedImgEl,
|
||||
offset,
|
||||
shouldRefresh,
|
||||
targetEl,
|
||||
}) => {
|
||||
const hasScalableSrc =
|
||||
isSvg ||
|
||||
imgSrc?.slice?.(0, 18) === "data:image/svg+xml" ||
|
||||
hasZoomImg ||
|
||||
!!(imgSrc && SRC_SVG_REGEX.test(imgSrc));
|
||||
|
||||
const imgRect = targetEl.getBoundingClientRect();
|
||||
const targetElComputedStyle = window.getComputedStyle(targetEl);
|
||||
|
||||
const styleImgRegular = getImgRegularStyle({
|
||||
containerHeight: imgRect.height,
|
||||
containerLeft: imgRect.left,
|
||||
containerTop: imgRect.top,
|
||||
containerWidth: imgRect.width,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight: loadedImgEl?.naturalHeight ?? imgRect.height,
|
||||
targetWidth: loadedImgEl?.naturalWidth ?? imgRect.width,
|
||||
});
|
||||
|
||||
const styleImgObjectFit =
|
||||
loadedImgEl && targetElComputedStyle.objectFit
|
||||
? getImgObjectFitStyle({
|
||||
containerHeight: imgRect.height,
|
||||
containerLeft: imgRect.left,
|
||||
containerTop: imgRect.top,
|
||||
containerWidth: imgRect.width,
|
||||
hasScalableSrc,
|
||||
objectFit: targetElComputedStyle.objectFit,
|
||||
objectPosition: targetElComputedStyle.objectPosition,
|
||||
offset,
|
||||
targetHeight: loadedImgEl.naturalHeight,
|
||||
targetWidth: loadedImgEl.naturalWidth,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const styleDivImg =
|
||||
loadedImgEl && testDiv(targetEl)
|
||||
? getDivImgStyle({
|
||||
backgroundPosition: targetElComputedStyle.backgroundPosition,
|
||||
backgroundSize: targetElComputedStyle.backgroundSize,
|
||||
containerHeight: imgRect.height,
|
||||
containerLeft: imgRect.left,
|
||||
containerTop: imgRect.top,
|
||||
containerWidth: imgRect.width,
|
||||
hasScalableSrc,
|
||||
offset,
|
||||
targetHeight: loadedImgEl.naturalHeight,
|
||||
targetWidth: loadedImgEl.naturalWidth,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const style = Object.assign(
|
||||
{},
|
||||
styleImgRegular,
|
||||
styleImgObjectFit,
|
||||
styleDivImg
|
||||
);
|
||||
|
||||
if (isZoomed) {
|
||||
const viewportX = window.innerWidth / 2;
|
||||
const viewportY = window.innerHeight / 2;
|
||||
|
||||
const childCenterX =
|
||||
parseFloat(String(style.left || 0)) +
|
||||
parseFloat(String(style.width || 0)) / 2;
|
||||
const childCenterY =
|
||||
parseFloat(String(style.top || 0)) +
|
||||
parseFloat(String(style.height || 0)) / 2;
|
||||
|
||||
const translateX = viewportX - childCenterX;
|
||||
const translateY = viewportY - childCenterY;
|
||||
|
||||
// For scenarios like resizing the browser window
|
||||
if (shouldRefresh) {
|
||||
style.transitionDuration = "0.01ms";
|
||||
}
|
||||
|
||||
style.transform = `translate(${translateX}px,${translateY}px) scale(1)`;
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
export interface GetStyleGhost {
|
||||
(imgEl: SupportedImage | null): CSSProperties;
|
||||
}
|
||||
Reference in New Issue
Block a user