perf: Remove useComponentSize from image and video node render
This commit is contained in:
@@ -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;
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
|||||||
29
app/scenes/Document/components/MeasuredContainer.tsx
Normal file
29
app/scenes/Document/components/MeasuredContainer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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" : ""}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user