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; const HEADING_OFFSET = 20;
type Props = { type Props = {
/** Whether the document is rendering full width or not. */
isFullWidth: boolean; isFullWidth: boolean;
/** The headings to render in the contents. */
headings: { headings: {
title: string; title: string;
level: number; 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 // calculate the minimum heading level and adjust all the headings to make
// that the top-most. This prevents the contents from being weirdly indented // 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( const minHeading = headings.reduce(
(memo, heading) => (heading.level < memo ? heading.level : memo), (memo, heading) => (heading.level < memo ? heading.level : memo),
Infinity Infinity

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,63 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
export default function useComponentSize(ref: React.RefObject<HTMLElement>): { const defaultRect = {
width: number; top: 0,
height: number; left: 0,
} { bottom: 0,
const [size, setSize] = useState({ right: 0,
width: ref.current?.clientWidth || 0, x: 0,
height: ref.current?.clientHeight || 0, y: 0,
}); width: 0,
height: 0,
};
export default function useComponentSize(
element: HTMLElement | null
): DOMRect | typeof defaultRect {
const [size, setSize] = useState(element?.getBoundingClientRect());
useEffect(() => { useEffect(() => {
const sizeObserver = new ResizeObserver((entries) => { const sizeObserver = new ResizeObserver((entries) => {
entries.forEach(({ target }) => { entries.forEach(({ target }) => {
if ( const rect = target?.getBoundingClientRect();
size.width !== target.clientWidth || setSize((state) =>
size.height !== target.clientHeight state?.width === rect?.width &&
) { state?.height === rect?.height &&
setSize({ width: target.clientWidth, height: target.clientHeight }); state?.x === rect?.x &&
} state?.y === rect?.y
? state
: rect
);
}); });
}); });
if (ref.current) { if (element) {
sizeObserver.observe(ref.current); sizeObserver.observe(element);
} }
return () => sizeObserver.disconnect(); 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) { export default function Berrycast(props: Props) {
const normalizedUrl = props.attrs.href.replace(/\/$/, ""); const normalizedUrl = props.attrs.href.replace(/\/$/, "");
const ref = React.useRef<HTMLDivElement>(null); const ref = React.useRef<HTMLDivElement>(null);
const { width } = useComponentSize(ref); const { width } = useComponentSize(ref.current);
return ( return (
<> <>