Correctly resize full width images when table of contents is opened/closed (#5826)

* stash

* restore

* Self review
This commit is contained in:
Tom Moor
2023-09-12 21:33:25 -04:00
committed by GitHub
parent d81db7e4f6
commit b80ee89588
6 changed files with 86 additions and 69 deletions

View File

@@ -10,7 +10,9 @@ import useWindowScrollPosition from "~/hooks/useWindowScrollPosition";
const HEADING_OFFSET = 20;
type Props = {
/** Whether the document is rendering full width or not. */
isFullWidth: boolean;
/** The headings to render in the contents. */
headings: {
title: string;
level: number;
@@ -45,7 +47,7 @@ export default function Contents({ headings, isFullWidth }: Props) {
// calculate the minimum heading level and adjust all the headings to make
// that the top-most. This prevents the contents from being weirdly indented
// if all of the headings in the document are level 3, for example.
// if all of the headings in the document start at level 3, for example.
const minHeading = headings.reduce(
(memo, heading) => (heading.level < memo ? heading.level : memo),
Infinity

View File

@@ -11,7 +11,6 @@ import { RefHandle } from "~/components/ContentEditable";
import Editor, { Props as EditorProps } from "~/components/Editor";
import Flex from "~/components/Flex";
import useFocusedComment from "~/hooks/useFocusedComment";
import useMobile from "~/hooks/useMobile";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import {
@@ -49,7 +48,6 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const titleRef = React.useRef<RefHandle>(null);
const { t } = useTranslation();
const match = useRouteMatch();
const isMobile = useMobile();
const focusedComment = useFocusedComment();
const { ui, comments, auth } = useStores();
const { user, team } = auth;
@@ -205,8 +203,8 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
}
extensions={extensions}
editorStyle={{
padding: document.fullWidth || isMobile ? "0 32px" : "0 64px",
margin: document.fullWidth || isMobile ? "0 -32px" : "0 -64px",
padding: "0 32px",
margin: "0 -32px",
paddingBottom: `calc(50vh - ${
childRef.current?.offsetHeight || 0
}px)`,

View File

@@ -7,24 +7,26 @@ import { s } from "../../styles";
import { sanitizeUrl } from "../../utils/urls";
import { ComponentProps } from "../types";
import ImageZoom from "./ImageZoom";
import useComponentSize from "./useComponentSize";
type DragDirection = "left" | "right";
const Image = (
props: ComponentProps & {
onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
onDownload?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onChangeSize?: (props: { width: number; height?: number }) => void;
children?: React.ReactElement;
view: EditorView;
}
) => {
type Props = ComponentProps & {
/** Callback triggered when the image is clicked */
onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
/** Callback triggered when the download button is clicked */
onDownload?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Callback triggered when the image is resized */
onChangeSize?: (props: { width: number; height?: number }) => void;
/** The editor view */
view: EditorView;
children?: React.ReactElement;
};
const Image = (props: Props) => {
const { isSelected, node, isEditable } = props;
const { src, layoutClass } = node.attrs;
const className = layoutClass ? `image image-${layoutClass}` : "image";
const [contentWidth, setContentWidth] = React.useState(
() => document.body.querySelector("#full-width-container")?.clientWidth || 0
);
const [loaded, setLoaded] = React.useState(false);
const [naturalWidth, setNaturalWidth] = React.useState(node.attrs.width);
const [naturalHeight, setNaturalHeight] = React.useState(node.attrs.height);
@@ -35,33 +37,18 @@ const Image = (
const [sizeAtDragStart, setSizeAtDragStart] = React.useState(size);
const [offset, setOffset] = React.useState(0);
const [dragging, setDragging] = React.useState<DragDirection>();
const [documentWidth, setDocumentWidth] = React.useState(
props.view ? getInnerWidth(props.view.dom) : 0
const documentBounds = useComponentSize(props.view.dom);
const containerBounds = useComponentSize(
document.body.querySelector("#full-width-container")
);
const maxWidth = layoutClass ? documentWidth / 3 : documentWidth;
const maxWidth = layoutClass
? documentBounds.width / 3
: documentBounds.width;
const isFullWidth = layoutClass === "full-width";
const isResizable = !!props.onChangeSize;
React.useLayoutEffect(() => {
if (!isResizable) {
return;
}
const handleResize = () => {
const contentWidth =
document.body.querySelector("#full-width-container")?.clientWidth || 0;
setContentWidth(contentWidth);
setDocumentWidth(props.view ? getInnerWidth(props.view.dom) : 0);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [props.view, isResizable]);
const constrainWidth = (width: number) => {
const minWidth = documentWidth * 0.1;
const minWidth = documentBounds.width * 0.1;
return Math.round(Math.min(maxWidth, Math.max(width, minWidth)));
};
@@ -75,7 +62,7 @@ const Image = (
diff = event.pageX - offset;
}
const grid = documentWidth / 10;
const grid = documentBounds.width / 10;
const newWidth = sizeAtDragStart.width + diff * 2;
const widthOnGrid = Math.round(newWidth / grid) * grid;
const constrainedWidth = constrainWidth(widthOnGrid);
@@ -153,12 +140,16 @@ const Image = (
}, [dragging, handlePointerMove, handlePointerUp, isResizable]);
const widthStyle = isFullWidth
? { width: contentWidth }
? { width: containerBounds.width }
: { width: size.width || "auto" };
const containerStyle = isFullWidth
? ({
"--offset": `${-(contentWidth - documentWidth) / 2}px`,
"--offset": `${-(
documentBounds.left -
containerBounds.left +
getPadding(props.view.dom)
)}px`,
} as React.CSSProperties)
: undefined;
@@ -235,14 +226,9 @@ const Image = (
);
};
function getInnerWidth(element: Element) {
function getPadding(element: Element) {
const computedStyle = window.getComputedStyle(element, null);
let width = element.clientWidth;
width -=
parseFloat(computedStyle.paddingLeft) +
parseFloat(computedStyle.paddingRight);
return width;
return parseFloat(computedStyle.paddingLeft);
}
function getPlaceholder(width: number, height: number) {

View File

@@ -450,7 +450,7 @@ li {
img {
max-width: 100vw;
max-height: 50vh;
max-height: min(450px, 50vh);
object-fit: cover;
object-position: center;
}

View File

@@ -1,32 +1,63 @@
import { useState, useEffect } from "react";
export default function useComponentSize(ref: React.RefObject<HTMLElement>): {
width: number;
height: number;
} {
const [size, setSize] = useState({
width: ref.current?.clientWidth || 0,
height: ref.current?.clientHeight || 0,
});
const defaultRect = {
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
width: 0,
height: 0,
};
export default function useComponentSize(
element: HTMLElement | null
): DOMRect | typeof defaultRect {
const [size, setSize] = useState(element?.getBoundingClientRect());
useEffect(() => {
const sizeObserver = new ResizeObserver((entries) => {
entries.forEach(({ target }) => {
if (
size.width !== target.clientWidth ||
size.height !== target.clientHeight
) {
setSize({ width: target.clientWidth, height: target.clientHeight });
}
const rect = target?.getBoundingClientRect();
setSize((state) =>
state?.width === rect?.width &&
state?.height === rect?.height &&
state?.x === rect?.x &&
state?.y === rect?.y
? state
: rect
);
});
});
if (ref.current) {
sizeObserver.observe(ref.current);
if (element) {
sizeObserver.observe(element);
}
return () => sizeObserver.disconnect();
}, [ref]);
}, [element]);
return size;
useEffect(() => {
const handleResize = () => {
const rect = element?.getBoundingClientRect();
setSize((state) =>
state?.width === rect?.width &&
state?.height === rect?.height &&
state?.x === rect?.x &&
state?.y === rect?.y
? state
: rect
);
};
window.addEventListener("click", handleResize);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("click", handleResize);
window.removeEventListener("resize", handleResize);
};
});
return size ?? defaultRect;
}

View File

@@ -8,7 +8,7 @@ const URL_REGEX = /^https:\/\/(www\.)?berrycast.com\/conversations\/(.*)$/;
export default function Berrycast(props: Props) {
const normalizedUrl = props.attrs.href.replace(/\/$/, "");
const ref = React.useRef<HTMLDivElement>(null);
const { width } = useComponentSize(ref);
const { width } = useComponentSize(ref.current);
return (
<>