diff --git a/app/scenes/Document/components/Contents.tsx b/app/scenes/Document/components/Contents.tsx index a7d31e5f9..fd3d39c7b 100644 --- a/app/scenes/Document/components/Contents.tsx +++ b/app/scenes/Document/components/Contents.tsx @@ -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 diff --git a/app/scenes/Document/components/Editor.tsx b/app/scenes/Document/components/Editor.tsx index d0a8e101c..64503026b 100644 --- a/app/scenes/Document/components/Editor.tsx +++ b/app/scenes/Document/components/Editor.tsx @@ -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) { const titleRef = React.useRef(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) { } 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)`, diff --git a/shared/editor/components/Image.tsx b/shared/editor/components/Image.tsx index 4f49c8684..15c945edc 100644 --- a/shared/editor/components/Image.tsx +++ b/shared/editor/components/Image.tsx @@ -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) => void; - onDownload?: (event: React.MouseEvent) => 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) => void; + /** Callback triggered when the download button is clicked */ + onDownload?: (event: React.MouseEvent) => 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(); - 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) { diff --git a/shared/editor/components/Styles.ts b/shared/editor/components/Styles.ts index b30a08bb9..78f233a9c 100644 --- a/shared/editor/components/Styles.ts +++ b/shared/editor/components/Styles.ts @@ -450,7 +450,7 @@ li { img { max-width: 100vw; - max-height: 50vh; + max-height: min(450px, 50vh); object-fit: cover; object-position: center; } diff --git a/shared/editor/components/useComponentSize.ts b/shared/editor/components/useComponentSize.ts index 69e857b5d..c03a87a5f 100644 --- a/shared/editor/components/useComponentSize.ts +++ b/shared/editor/components/useComponentSize.ts @@ -1,32 +1,63 @@ import { useState, useEffect } from "react"; -export default function useComponentSize(ref: React.RefObject): { - 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; } diff --git a/shared/editor/embeds/Berrycast.tsx b/shared/editor/embeds/Berrycast.tsx index 260a22b23..f42d66096 100644 --- a/shared/editor/embeds/Berrycast.tsx +++ b/shared/editor/embeds/Berrycast.tsx @@ -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(null); - const { width } = useComponentSize(ref); + const { width } = useComponentSize(ref.current); return ( <>