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"; import { useState, useEffect } from "react";
export default function useComponentSize(ref: React.RefObject<HTMLElement>): { export default function useComponentSize(
ref: React.RefObject<HTMLElement | null>
): {
width: number; width: number;
height: number; height: number;
} { } {

View File

@@ -17,7 +17,6 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import useComponentSize from "@shared/editor/components/hooks/useComponentSize";
import { s } from "@shared/styles"; import { s } from "@shared/styles";
import { NavigationNode } from "@shared/types"; import { NavigationNode } from "@shared/types";
import { ProsemirrorHelper, Heading } from "@shared/utils/ProsemirrorHelper"; import { ProsemirrorHelper, Heading } from "@shared/utils/ProsemirrorHelper";
@@ -54,6 +53,7 @@ import Editor from "./Editor";
import Header from "./Header"; import Header from "./Header";
import KeyboardShortcutsButton from "./KeyboardShortcutsButton"; import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
import MarkAsViewed from "./MarkAsViewed"; import MarkAsViewed from "./MarkAsViewed";
import { MeasuredContainer } from "./MeasuredContainer";
import Notices from "./Notices"; import Notices from "./Notices";
import PublicReferences from "./PublicReferences"; import PublicReferences from "./PublicReferences";
import References from "./References"; 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} key={revision ? revision.id : document.id}
column column
auto auto
@@ -475,7 +477,9 @@ class DocumentScene extends React.Component<Props> {
onSave={this.onSave} onSave={this.onSave}
headings={this.headings} headings={this.headings}
/> />
<MaxWidth <MeasuredContainer
as={MaxWidth}
name="document"
archived={document.isArchived} archived={document.isArchived}
showContents={showContents} showContents={showContents}
isEditing={!readOnly} isEditing={!readOnly}
@@ -551,7 +555,7 @@ class DocumentScene extends React.Component<Props> {
)} )}
</Flex> </Flex>
</React.Suspense> </React.Suspense>
</MaxWidth> </MeasuredContainer>
{isShare && {isShare &&
!parseDomain(window.location.origin).custom && !parseDomain(window.location.origin).custom &&
!auth.user && ( !auth.user && (
@@ -564,7 +568,7 @@ class DocumentScene extends React.Component<Props> {
<ConnectionStatus /> <ConnectionStatus />
</Footer> </Footer>
)} )}
</FullWidthContainer> </MeasuredContainer>
</ErrorBoundary> </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))); 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 { ComponentProps } from "../types";
import ImageZoom from "./ImageZoom"; import ImageZoom from "./ImageZoom";
import { ResizeLeft, ResizeRight } from "./ResizeHandle"; import { ResizeLeft, ResizeRight } from "./ResizeHandle";
import useComponentSize from "./hooks/useComponentSize";
import useDragResize from "./hooks/useDragResize"; import useDragResize from "./hooks/useDragResize";
type Props = ComponentProps & { type Props = ComponentProps & {
@@ -29,18 +28,16 @@ const Image = (props: Props) => {
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);
const documentBounds = useComponentSize(props.view.dom); const ref = React.useRef<HTMLDivElement>(null);
const maxWidth = documentBounds.width;
const { width, height, setSize, handlePointerDown, dragging } = useDragResize( const { width, height, setSize, handlePointerDown, dragging } = useDragResize(
{ {
width: node.attrs.width ?? naturalWidth, width: node.attrs.width ?? naturalWidth,
height: node.attrs.height ?? naturalHeight, height: node.attrs.height ?? naturalHeight,
minWidth: documentBounds.width * 0.1,
maxWidth,
naturalWidth, naturalWidth,
naturalHeight, naturalHeight,
gridWidth: documentBounds.width / 20, gridSnap: 5,
onChangeSize, onChangeSize,
ref,
} }
); );
@@ -61,7 +58,7 @@ const Image = (props: Props) => {
: { width: width || "auto" }; : { width: width || "auto" };
return ( return (
<div contentEditable={false} className={className}> <div contentEditable={false} className={className} ref={ref}>
<ImageWrapper <ImageWrapper
isFullWidth={isFullWidth} isFullWidth={isFullWidth}
className={isSelected || dragging ? "ProseMirror-selectednode" : ""} className={isSelected || dragging ? "ProseMirror-selectednode" : ""}

View File

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

View File

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