Correctly resize full width images when table of contents is opened/closed (#5826)
* stash * restore * Self review
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -450,7 +450,7 @@ li {
|
||||
|
||||
img {
|
||||
max-width: 100vw;
|
||||
max-height: 50vh;
|
||||
max-height: min(450px, 50vh);
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user