From 2485bed3508b194137f058064cb36d1ba1d2c41c Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 24 Nov 2022 21:06:09 +0530 Subject: [PATCH] feat: resize images in super editor (#2052) --- .../Components/FilePreview/FilePreview.tsx | 15 +- .../Components/FilePreview/ImagePreview.tsx | 131 ++++++++++++++---- .../FilePreview/ImageZoomLevelProps.tsx | 4 + .../FilePreview/PreviewComponent.tsx | 22 ++- .../Nodes/FileComponent.tsx | 29 +++- .../EncryptedFilePlugin/Nodes/FileNode.tsx | 24 +++- .../Nodes/SerializedFileNode.tsx | 1 + 7 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 packages/web/src/javascripts/Components/FilePreview/ImageZoomLevelProps.tsx diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx index 8f1febf9b..c58c8a7ba 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreview.tsx @@ -8,13 +8,15 @@ import { isFileTypePreviewable } from './isFilePreviewable' import PreviewComponent from './PreviewComponent' import Button from '../Button/Button' import { ProtectedIllustration } from '@standardnotes/icons' +import { ImageZoomLevelProps } from './ImageZoomLevelProps' type Props = { application: WebApplication file: FileItem -} + isEmbeddedInSuper?: boolean +} & ImageZoomLevelProps -const FilePreview = ({ file, application }: Props) => { +const FilePreview = ({ file, application, isEmbeddedInSuper = false, imageZoomLevel, setImageZoomLevel }: Props) => { const [isAuthorized, setIsAuthorized] = useState(application.isAuthorizedToRenderItem(file)) const isFilePreviewable = useMemo(() => { @@ -112,7 +114,14 @@ const FilePreview = ({ file, application }: Props) => { Loading file... ) : downloadedBytes ? ( - + ) : ( = ({ objectUrl }) => { - const initialImgHeightRef = useRef() +const MinimumZoomPercent = 10 +const DefaultZoomPercent = 100 +const MaximumZoomPercent = 1000 +const ZoomPercentModifier = 10 +const PercentageDivisor = 100 - const [imageZoomPercent, setImageZoomPercent] = useState(100) +const ImagePreview: FunctionComponent = ({ + objectUrl, + isEmbeddedInSuper, + imageZoomLevel, + setImageZoomLevel, +}) => { + const [imageHeight, setImageHeight] = useState(0) + const [imageZoomPercent, setImageZoomPercent] = useState(imageZoomLevel ? imageZoomLevel : DefaultZoomPercent) + const [isZoomInputVisible, setIsZoomInputVisible] = useState(false) + + useEffect(() => { + setImageZoomPercent(imageZoomLevel ? imageZoomLevel : DefaultZoomPercent) + }, [imageZoomLevel]) + + const setImageZoom = useCallback( + (zoomLevel: number) => { + setImageZoomPercent(zoomLevel) + setImageZoomLevel?.(zoomLevel) + }, + [setImageZoomLevel], + ) + + useEffect(() => { + const image = new Image() + image.src = objectUrl + image.onload = () => { + setImageHeight(image.height) + } + }, [objectUrl]) + + const heightIfEmbedded = imageHeight * (imageZoomPercent / PercentageDivisor) return ( -
-
+
+
= ({ objectUrl }) => { maxWidth: 'none', }), }} - ref={(imgElement) => { - if (!initialImgHeightRef.current) { - initialImgHeightRef.current = imgElement?.height - } - }} />
-
- Zoom: +
+ {isEmbeddedInSuper ? 'Size' : 'Zoom'}: { - setImageZoomPercent((percent) => { - const newPercent = percent - 10 - if (newPercent >= 10) { - return newPercent - } else { - return percent - } - }) + const newPercent = imageZoomPercent - ZoomPercentModifier + if (newPercent >= ZoomPercentModifier) { + setImageZoom(newPercent) + } else { + setImageZoom(imageZoomPercent) + } }} /> - {imageZoomPercent}% + {isZoomInputVisible ? ( +
+ { + event.stopPropagation() + if (event.key === 'Enter') { + const value = parseInt(event.currentTarget.value) + if (value >= MinimumZoomPercent && value <= MaximumZoomPercent) { + setImageZoom(value) + } + setIsZoomInputVisible(false) + } + }} + onBlur={(event) => { + setIsZoomInputVisible(false) + const value = parseInt(event.currentTarget.value) + if (value >= MinimumZoomPercent && value <= MaximumZoomPercent) { + setImageZoom(value) + } + }} + /> + % +
+ ) : ( + + )} { - setImageZoomPercent((percent) => percent + 10) + setImageZoom(imageZoomPercent + ZoomPercentModifier) }} />
diff --git a/packages/web/src/javascripts/Components/FilePreview/ImageZoomLevelProps.tsx b/packages/web/src/javascripts/Components/FilePreview/ImageZoomLevelProps.tsx new file mode 100644 index 000000000..461e377f3 --- /dev/null +++ b/packages/web/src/javascripts/Components/FilePreview/ImageZoomLevelProps.tsx @@ -0,0 +1,4 @@ +export type ImageZoomLevelProps = { + imageZoomLevel?: number + setImageZoomLevel?: (zoomLevel: number) => void +} diff --git a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx index 10c0116de..ad6a8bca1 100644 --- a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx @@ -7,6 +7,7 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { createObjectURLWithRef } from './CreateObjectURLWithRef' import ImagePreview from './ImagePreview' +import { ImageZoomLevelProps } from './ImageZoomLevelProps' import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable' import TextPreview from './TextPreview' @@ -14,9 +15,17 @@ type Props = { application: WebApplication file: FileItem bytes: Uint8Array -} + isEmbeddedInSuper: boolean +} & ImageZoomLevelProps -const PreviewComponent: FunctionComponent = ({ application, file, bytes }) => { +const PreviewComponent: FunctionComponent = ({ + application, + file, + bytes, + isEmbeddedInSuper, + imageZoomLevel, + setImageZoomLevel, +}) => { const { selectedPane } = useResponsiveAppPane() const objectUrlRef = useRef() @@ -73,7 +82,14 @@ const PreviewComponent: FunctionComponent = ({ application, file, bytes } } if (file.mimeType.startsWith('image/')) { - return + return ( + + ) } if (file.mimeType.startsWith('video/')) { diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx index d72e96565..ffafd8272 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx @@ -1,9 +1,10 @@ import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ElementFormatType, NodeKey } from 'lexical' import { useApplication } from '@/Components/ApplicationView/ApplicationProvider' import FilePreview from '@/Components/FilePreview/FilePreview' import { FileItem } from '@standardnotes/snjs' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' export type FileComponentProps = Readonly<{ className: Readonly<{ @@ -13,10 +14,13 @@ export type FileComponentProps = Readonly<{ format: ElementFormatType | null nodeKey: NodeKey fileUuid: string + zoomLevel: number + setZoomLevel: (zoomLevel: number) => void }> -export function FileComponent({ className, format, nodeKey, fileUuid }: FileComponentProps) { +export function FileComponent({ className, format, nodeKey, fileUuid, zoomLevel, setZoomLevel }: FileComponentProps) { const application = useApplication() + const [editor] = useLexicalComposerContext() const file = useMemo(() => application.items.findItem(fileUuid), [application, fileUuid]) const [canLoad, setCanLoad] = useState(false) @@ -53,13 +57,32 @@ export function FileComponent({ className, format, nodeKey, fileUuid }: FileComp } }, [blockObserver]) + const setImageZoomLevel = useCallback( + (zoomLevel: number) => { + editor.update(() => { + setZoomLevel(zoomLevel) + }) + }, + [editor, setZoomLevel], + ) + if (!file) { return
Unable to find file {fileUuid}
} return ( -
{canLoad && }
+
+ {canLoad && ( + + )} +
) } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx index 7de15e462..0f86d135b 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx @@ -7,18 +7,20 @@ import { ItemNodeInterface } from '../../ItemNodeInterface' export class FileNode extends DecoratorBlockNode implements ItemNodeInterface { __id: string + __zoomLevel: number static getType(): string { return 'snfile' } static clone(node: FileNode): FileNode { - return new FileNode(node.__id, node.__format, node.__key) + return new FileNode(node.__id, node.__format, node.__key, node.__zoomLevel) } static importJSON(serializedNode: SerializedFileNode): FileNode { const node = $createFileNode(serializedNode.fileUuid) node.setFormat(serializedNode.format) + node.setZoomLevel(serializedNode.zoomLevel) return node } @@ -28,6 +30,7 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface { fileUuid: this.getId(), version: 1, type: 'snfile', + zoomLevel: this.__zoomLevel, } } @@ -53,9 +56,10 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface { return { element } } - constructor(id: string, format?: ElementFormatType, key?: NodeKey) { + constructor(id: string, format?: ElementFormatType, key?: NodeKey, zoomLevel?: number) { super(format, key) this.__id = id + this.__zoomLevel = zoomLevel || 100 } getId(): string { @@ -66,6 +70,11 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface { return `[File: ${this.__id}]` } + setZoomLevel(zoomLevel: number): void { + const writable = this.getWritable() + writable.__zoomLevel = zoomLevel + } + decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element { const embedBlockTheme = config.theme.embedBlock || {} const className = { @@ -73,6 +82,15 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface { focus: embedBlockTheme.focus || '', } - return + return ( + + ) } } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx index 7b0252fa0..a85caac10 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx @@ -6,6 +6,7 @@ export type SerializedFileNode = Spread< fileUuid: string version: 1 type: 'snfile' + zoomLevel: number }, SerializedDecoratorBlockNode >