perf: Remove useComponentSize from image and video node render

This commit is contained in:
Tom Moor
2024-06-01 11:13:03 -04:00
parent 009458e435
commit f2e9c0ab23
6 changed files with 87 additions and 46 deletions

View File

@@ -1,6 +1,8 @@
import { useState, useEffect } from "react";
export default function useComponentSize(ref: React.RefObject<HTMLElement>): {
export default function useComponentSize(
ref: React.RefObject<HTMLElement | null>
): {
width: number;
height: number;
} {

View File

@@ -17,7 +17,6 @@ import {
import { toast } from "sonner";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import useComponentSize from "@shared/editor/components/hooks/useComponentSize";
import { s } from "@shared/styles";
import { NavigationNode } from "@shared/types";
import { ProsemirrorHelper, Heading } from "@shared/utils/ProsemirrorHelper";
@@ -54,6 +53,7 @@ import Editor from "./Editor";
import Header from "./Header";
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
import MarkAsViewed from "./MarkAsViewed";
import { MeasuredContainer } from "./MeasuredContainer";
import Notices from "./Notices";
import PublicReferences from "./PublicReferences";
import References from "./References";
@@ -438,7 +438,9 @@ class DocumentScene extends React.Component<Props> {
}
}}
/>
<FullWidthContainer
<MeasuredContainer
as={Background}
name="container"
key={revision ? revision.id : document.id}
column
auto
@@ -475,7 +477,9 @@ class DocumentScene extends React.Component<Props> {
onSave={this.onSave}
headings={this.headings}
/>
<MaxWidth
<MeasuredContainer
as={MaxWidth}
name="document"
archived={document.isArchived}
showContents={showContents}
isEditing={!readOnly}
@@ -551,7 +555,7 @@ class DocumentScene extends React.Component<Props> {
)}
</Flex>
</React.Suspense>
</MaxWidth>
</MeasuredContainer>
{isShare &&
!parseDomain(window.location.origin).custom &&
!auth.user && (
@@ -564,7 +568,7 @@ class DocumentScene extends React.Component<Props> {
<ConnectionStatus />
</Footer>
)}
</FullWidthContainer>
</MeasuredContainer>
</ErrorBoundary>
);
}
@@ -622,20 +626,4 @@ const MaxWidth = styled(Flex)<MaxWidthProps>`
`};
`;
const FullWidthContainer = (props: React.ComponentProps<typeof Background>) => {
const ref = React.useRef(null);
const rect = useComponentSize(ref.current);
return (
<Background
{...props}
ref={ref}
style={{
"--container-width": `${rect.width}px`,
"--container-left": `${rect.left}px`,
}}
/>
);
};
export default withTranslation()(withStores(withRouter(DocumentScene)));

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import useComponentSize from "@shared/editor/components/hooks/useComponentSize";
export const MeasuredContainer = <T extends React.ElementType>({
as: As,
name,
children,
...rest
}: {
as: T;
name: string;
children?: React.ReactNode;
} & React.ComponentProps<T>) => {
const ref = React.useRef<HTMLElement>(null);
const rect = useComponentSize(ref.current);
return (
<As
{...rest}
ref={ref}
style={{
[`--${name}-width`]: `${rect.width}px`,
[`--${name}-height`]: `${rect.height}px`,
}}
>
{children}
</As>
);
};

View File

@@ -7,7 +7,6 @@ import { sanitizeUrl } from "../../utils/urls";
import { ComponentProps } from "../types";
import ImageZoom from "./ImageZoom";
import { ResizeLeft, ResizeRight } from "./ResizeHandle";
import useComponentSize from "./hooks/useComponentSize";
import useDragResize from "./hooks/useDragResize";
type Props = ComponentProps & {
@@ -29,18 +28,16 @@ const Image = (props: Props) => {
const [loaded, setLoaded] = React.useState(false);
const [naturalWidth, setNaturalWidth] = React.useState(node.attrs.width);
const [naturalHeight, setNaturalHeight] = React.useState(node.attrs.height);
const documentBounds = useComponentSize(props.view.dom);
const maxWidth = documentBounds.width;
const ref = React.useRef<HTMLDivElement>(null);
const { width, height, setSize, handlePointerDown, dragging } = useDragResize(
{
width: node.attrs.width ?? naturalWidth,
height: node.attrs.height ?? naturalHeight,
minWidth: documentBounds.width * 0.1,
maxWidth,
naturalWidth,
naturalHeight,
gridWidth: documentBounds.width / 20,
gridSnap: 5,
onChangeSize,
ref,
}
);
@@ -61,7 +58,7 @@ const Image = (props: Props) => {
: { width: width || "auto" };
return (
<div contentEditable={false} className={className}>
<div contentEditable={false} className={className} ref={ref}>
<ImageWrapper
isFullWidth={isFullWidth}
className={isSelected || dragging ? "ProseMirror-selectednode" : ""}

View File

@@ -3,7 +3,6 @@ import styled, { css } from "styled-components";
import { sanitizeUrl } from "../../utils/urls";
import { ComponentProps } from "../types";
import { ResizeLeft, ResizeRight } from "./ResizeHandle";
import useComponentSize from "./hooks/useComponentSize";
import useDragResize from "./hooks/useDragResize";
type Props = ComponentProps & {
@@ -16,19 +15,18 @@ export default function Video(props: Props) {
const { isSelected, node, isEditable, children, onChangeSize } = props;
const [naturalWidth] = React.useState(node.attrs.width);
const [naturalHeight] = React.useState(node.attrs.height);
const documentBounds = useComponentSize(props.view.dom);
const ref = React.useRef<HTMLDivElement>(null);
const isResizable = !!onChangeSize;
const { width, height, setSize, handlePointerDown, dragging } = useDragResize(
{
width: node.attrs.width ?? naturalWidth,
height: node.attrs.height ?? naturalHeight,
minWidth: documentBounds.width * 0.1,
maxWidth: documentBounds.width,
naturalWidth,
naturalHeight,
gridWidth: documentBounds.width / 20,
gridSnap: 5,
onChangeSize,
ref,
}
);
@@ -48,7 +46,7 @@ export default function Video(props: Props) {
};
return (
<div contentEditable={false}>
<div contentEditable={false} ref={ref}>
<VideoWrapper
className={isSelected ? "ProseMirror-selectednode" : ""}
style={style}

View File

@@ -4,39 +4,56 @@ type DragDirection = "left" | "right";
type SizeState = { width: number; height?: number };
/**
* Hook for resizing an element by dragging its sides.
*/
type ReturnValue = {
/** Event handler for pointer down event on the resize handle. */
handlePointerDown: (
dragging: DragDirection
) => (event: React.PointerEvent<HTMLDivElement>) => void;
/** Handler to set the new size of the element from outside. */
setSize: React.Dispatch<React.SetStateAction<SizeState>>;
/** Whether the element is currently being resized. */
dragging: boolean;
/** The current width of the element. */
width: number;
/** The current height of the element. */
height?: number;
};
type Props = {
type Params = {
/** Callback triggered when the image is resized */
onChangeSize?: undefined | ((size: SizeState) => void);
/** The initial width of the element. */
width: number;
/** The initial height of the element. */
height: number;
/** The natural width of the element. */
naturalWidth: number;
/** The natural height of the element. */
naturalHeight: number;
minWidth: number;
maxWidth: number;
gridWidth: number;
/** The percentage of the grid to snap the element to. */
gridSnap: 5;
/** A reference to the element being resized. */
ref: React.RefObject<HTMLDivElement>;
};
export default function useDragResize(props: Props): ReturnValue {
export default function useDragResize(props: Params): ReturnValue {
const [size, setSize] = React.useState<SizeState>({
width: props.width,
height: props.height,
});
const [maxWidth, setMaxWidth] = React.useState(Infinity);
const [offset, setOffset] = React.useState(0);
const [sizeAtDragStart, setSizeAtDragStart] = React.useState(size);
const [dragging, setDragging] = React.useState<DragDirection>();
const isResizable = !!props.onChangeSize;
const constrainWidth = (width: number) =>
Math.round(Math.min(props.maxWidth, Math.max(width, props.minWidth)));
const constrainWidth = (width: number, max: number) => {
const minWidth = Math.min(props.naturalWidth, (props.gridSnap / 100) * max);
return Math.round(Math.min(max, Math.max(width, minWidth)));
};
const handlePointerMove = (event: PointerEvent) => {
event.preventDefault();
@@ -48,10 +65,10 @@ export default function useDragResize(props: Props): ReturnValue {
diff = event.pageX - offset;
}
const gridWidth = (props.gridSnap / 100) * maxWidth;
const newWidth = sizeAtDragStart.width + diff * 2;
const widthOnGrid =
Math.round(newWidth / props.gridWidth) * props.gridWidth;
const constrainedWidth = constrainWidth(widthOnGrid);
const widthOnGrid = Math.round(newWidth / gridWidth) * gridWidth;
const constrainedWidth = constrainWidth(widthOnGrid, maxWidth);
const aspectRatio = props.naturalHeight / props.naturalWidth;
setSize({
@@ -88,8 +105,18 @@ export default function useDragResize(props: Props): ReturnValue {
(event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
// Calculate constraints once at the start of dragging as it's relatively expensive operation
const max = props.ref.current
? parseInt(
getComputedStyle(props.ref.current).getPropertyValue(
"--document-width"
)
)
: Infinity;
setMaxWidth(max);
setSizeAtDragStart({
width: constrainWidth(size.width),
width: constrainWidth(size.width, max),
height: size.height,
});
setOffset(event.pageX);