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

@@ -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);