fix: image preview (#2818)
This commit is contained in:
@@ -22,74 +22,6 @@ export const useZoomControls = ({
|
|||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zoomIn = useCallback(() => {
|
|
||||||
const image = imageRef.current;
|
|
||||||
|
|
||||||
if (image && currentScale < 2) {
|
|
||||||
const newScale = currentScale + 0.1;
|
|
||||||
setCurrentScale(newScale);
|
|
||||||
image.style.width = `${image.naturalWidth * newScale}px`;
|
|
||||||
image.style.height = `${image.naturalHeight * newScale}px`;
|
|
||||||
}
|
|
||||||
}, [imageRef, currentScale]);
|
|
||||||
|
|
||||||
const zoomOut = useCallback(() => {
|
|
||||||
const image = imageRef.current;
|
|
||||||
if (image && currentScale > 0.2) {
|
|
||||||
const newScale = currentScale - 0.1;
|
|
||||||
setCurrentScale(newScale);
|
|
||||||
image.style.width = `${image.naturalWidth * newScale}px`;
|
|
||||||
image.style.height = `${image.naturalHeight * newScale}px`;
|
|
||||||
const zoomedWidth = image.naturalWidth * newScale;
|
|
||||||
const zoomedHeight = image.naturalHeight * newScale;
|
|
||||||
const containerWidth = window.innerWidth;
|
|
||||||
const containerHeight = window.innerHeight;
|
|
||||||
if (zoomedWidth > containerWidth || zoomedHeight > containerHeight) {
|
|
||||||
image.style.transform = `translate(0px, 0px)`;
|
|
||||||
setImagePos({ x: 0, y: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [imageRef, currentScale]);
|
|
||||||
|
|
||||||
const checkZoomSize = useCallback(() => {
|
|
||||||
const { current: zoomArea } = zoomRef;
|
|
||||||
if (zoomArea) {
|
|
||||||
const image = zoomArea.querySelector('img');
|
|
||||||
if (image) {
|
|
||||||
const zoomedWidth = image.naturalWidth * currentScale;
|
|
||||||
const zoomedHeight = image.naturalHeight * currentScale;
|
|
||||||
const containerWidth = window.innerWidth;
|
|
||||||
const containerHeight = window.innerHeight;
|
|
||||||
setIsZoomedBigger(
|
|
||||||
zoomedWidth > containerWidth || zoomedHeight > containerHeight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [currentScale, zoomRef]);
|
|
||||||
|
|
||||||
const resetZoom = useCallback(() => {
|
|
||||||
const image = imageRef.current;
|
|
||||||
if (image) {
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const margin = 0.2;
|
|
||||||
|
|
||||||
const availableWidth = viewportWidth * (1 - margin);
|
|
||||||
const availableHeight = viewportHeight * (1 - margin);
|
|
||||||
|
|
||||||
const widthRatio = availableWidth / image.naturalWidth;
|
|
||||||
const heightRatio = availableHeight / image.naturalHeight;
|
|
||||||
|
|
||||||
const newScale = Math.min(widthRatio, heightRatio);
|
|
||||||
setCurrentScale(newScale);
|
|
||||||
image.style.width = `${image.naturalWidth * newScale}px`;
|
|
||||||
image.style.height = `${image.naturalHeight * newScale}px`;
|
|
||||||
image.style.transform = 'translate(0px, 0px)';
|
|
||||||
setImagePos({ x: 0, y: 0 });
|
|
||||||
checkZoomSize();
|
|
||||||
}
|
|
||||||
}, [checkZoomSize, imageRef]);
|
|
||||||
|
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(event: ReactMouseEvent) => {
|
(event: ReactMouseEvent) => {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
@@ -170,6 +102,86 @@ export const useZoomControls = ({
|
|||||||
}
|
}
|
||||||
}, [isDragging, dragEndImpl]);
|
}, [isDragging, dragEndImpl]);
|
||||||
|
|
||||||
|
const checkZoomSize = useCallback(() => {
|
||||||
|
const { current: zoomArea } = zoomRef;
|
||||||
|
if (zoomArea) {
|
||||||
|
const image = zoomArea.querySelector('img');
|
||||||
|
if (image) {
|
||||||
|
const zoomedWidth = image.naturalWidth * currentScale;
|
||||||
|
const zoomedHeight = image.naturalHeight * currentScale;
|
||||||
|
const containerWidth = window.innerWidth;
|
||||||
|
const containerHeight = window.innerHeight;
|
||||||
|
setIsZoomedBigger(
|
||||||
|
zoomedWidth > containerWidth || zoomedHeight > containerHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentScale, zoomRef]);
|
||||||
|
|
||||||
|
const zoomIn = useCallback(() => {
|
||||||
|
const image = imageRef.current;
|
||||||
|
|
||||||
|
if (image && currentScale < 2) {
|
||||||
|
const newScale = currentScale + 0.1;
|
||||||
|
setCurrentScale(newScale);
|
||||||
|
image.style.width = `${image.naturalWidth * newScale}px`;
|
||||||
|
image.style.height = `${image.naturalHeight * newScale}px`;
|
||||||
|
}
|
||||||
|
}, [imageRef, currentScale]);
|
||||||
|
|
||||||
|
const zoomOut = useCallback(() => {
|
||||||
|
const image = imageRef.current;
|
||||||
|
if (image && currentScale > 0.2) {
|
||||||
|
const newScale = currentScale - 0.1;
|
||||||
|
setCurrentScale(newScale);
|
||||||
|
image.style.width = `${image.naturalWidth * newScale}px`;
|
||||||
|
image.style.height = `${image.naturalHeight * newScale}px`;
|
||||||
|
const zoomedWidth = image.naturalWidth * newScale;
|
||||||
|
const zoomedHeight = image.naturalHeight * newScale;
|
||||||
|
const containerWidth = window.innerWidth;
|
||||||
|
const containerHeight = window.innerHeight;
|
||||||
|
if (zoomedWidth > containerWidth || zoomedHeight > containerHeight) {
|
||||||
|
image.style.transform = `translate(0px, 0px)`;
|
||||||
|
setImagePos({ x: 0, y: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [imageRef, currentScale]);
|
||||||
|
|
||||||
|
const resetZoom = useCallback(() => {
|
||||||
|
const image = imageRef.current;
|
||||||
|
if (image) {
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const margin = 0.2;
|
||||||
|
|
||||||
|
const availableWidth = viewportWidth * (1 - margin);
|
||||||
|
const availableHeight = viewportHeight * (1 - margin);
|
||||||
|
|
||||||
|
const widthRatio = availableWidth / image.naturalWidth;
|
||||||
|
const heightRatio = availableHeight / image.naturalHeight;
|
||||||
|
|
||||||
|
const newScale = Math.min(widthRatio, heightRatio);
|
||||||
|
setCurrentScale(newScale);
|
||||||
|
image.style.width = `${image.naturalWidth * newScale}px`;
|
||||||
|
image.style.height = `${image.naturalHeight * newScale}px`;
|
||||||
|
image.style.transform = 'translate(0px, 0px)';
|
||||||
|
setImagePos({ x: 0, y: 0 });
|
||||||
|
checkZoomSize();
|
||||||
|
}
|
||||||
|
}, [imageRef, checkZoomSize]);
|
||||||
|
|
||||||
|
const resetScale = useCallback(() => {
|
||||||
|
const image = imageRef.current;
|
||||||
|
if (image) {
|
||||||
|
setCurrentScale(1);
|
||||||
|
image.style.width = `${image.naturalWidth}px`;
|
||||||
|
image.style.height = `${image.naturalHeight}px`;
|
||||||
|
image.style.transform = 'translate(0px, 0px)';
|
||||||
|
setImagePos({ x: 0, y: 0 });
|
||||||
|
}
|
||||||
|
}, [imageRef]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = (event: WheelEvent) => {
|
const handleScroll = (event: WheelEvent) => {
|
||||||
const { deltaY } = event;
|
const { deltaY } = event;
|
||||||
@@ -201,6 +213,7 @@ export const useZoomControls = ({
|
|||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
resetZoom,
|
resetZoom,
|
||||||
|
resetScale,
|
||||||
isZoomedBigger,
|
isZoomedBigger,
|
||||||
currentScale,
|
currentScale,
|
||||||
handleDragStart,
|
handleDragStart,
|
||||||
|
|||||||
@@ -1,18 +1,45 @@
|
|||||||
import { baseTheme } from '@toeverything/theme';
|
import { baseTheme } from '@toeverything/theme';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const imagePreviewModalStyle = style({
|
const fadeInAnimation = keyframes({
|
||||||
|
from: { opacity: 0 },
|
||||||
|
to: { opacity: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fadeOutAnimation = keyframes({
|
||||||
|
from: { opacity: 1 },
|
||||||
|
to: { opacity: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const imagePreviewBackgroundStyle = style({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
zIndex: baseTheme.zIndexModal,
|
zIndex: baseTheme.zIndexModal,
|
||||||
|
background: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const imagePreviewModalStyle = style({
|
||||||
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
// background: 'var(--affine-background-modal-color)',
|
});
|
||||||
background: 'rgba(0,0,0,0.75)',
|
|
||||||
|
export const loaded = style({
|
||||||
|
opacity: 0,
|
||||||
|
animationName: fadeInAnimation,
|
||||||
|
animationDuration: '0.25s',
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const unloaded = style({
|
||||||
|
opacity: 1,
|
||||||
|
animationName: fadeOutAnimation,
|
||||||
|
animationDuration: '0.25s',
|
||||||
|
animationFillMode: 'forwards',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const imagePreviewModalCloseButtonStyle = style({
|
export const imagePreviewModalCloseButtonStyle = style({
|
||||||
@@ -33,6 +60,9 @@ export const imagePreviewModalCloseButtonStyle = style({
|
|||||||
color: 'var(--affine-icon-color)',
|
color: 'var(--affine-icon-color)',
|
||||||
transition: 'background 0.2s ease-in-out',
|
transition: 'background 0.2s ease-in-out',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
marginTop: '38px',
|
||||||
|
marginRight: '38px',
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const imagePreviewModalGoStyle = style({
|
export const imagePreviewModalGoStyle = style({
|
||||||
@@ -86,28 +116,48 @@ export const imagePreviewActionBarStyle = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '16px 0',
|
|
||||||
backgroundColor: 'var(--affine-white)',
|
backgroundColor: 'var(--affine-white)',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.3)',
|
boxShadow: '2px 2px 4px rgba(0, 0, 0, 0.3)',
|
||||||
maxWidth: 'max-content',
|
maxWidth: 'max-content',
|
||||||
|
minHeight: '44px',
|
||||||
|
maxHeight: '44px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const groupStyle = style({
|
export const groupStyle = style({
|
||||||
display: 'inline-flex',
|
padding: '10px 0',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: 'var(--affine-white)',
|
|
||||||
borderLeft: '1px solid #E3E2E4',
|
borderLeft: '1px solid #E3E2E4',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const buttonStyle = style({
|
export const buttonStyle = style({
|
||||||
paddingLeft: '10px',
|
minWidth: '24px',
|
||||||
paddingRight: '10px',
|
height: '24px',
|
||||||
|
margin: '10px 6px',
|
||||||
|
padding: '0 0',
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
backgroundSize: '24px 24px',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const scaleIndicatorStyle = style({
|
export const buttonIconStyle = style({
|
||||||
margin: '0 8px',
|
width: '20px',
|
||||||
|
height: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const scaleIndicatorButtonStyle = style({
|
||||||
|
minHeight: '100%',
|
||||||
|
maxWidth: 'max-content',
|
||||||
|
fontSize: '12px',
|
||||||
|
padding: '5px 5px',
|
||||||
|
|
||||||
|
':hover': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const imageBottomContainerStyle = style({
|
export const imageBottomContainerStyle = style({
|
||||||
@@ -126,3 +176,8 @@ export const captionStyle = style({
|
|||||||
padding: '10px',
|
padding: '10px',
|
||||||
marginBottom: '21px',
|
marginBottom: '21px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const suspenseFallbackStyle = style({
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'opacity 2s ease-in-out',
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { EmbedBlockDoubleClickData } from '@blocksuite/blocks';
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
export const previewBlockIdAtom = atom<string | null>(null);
|
export const previewBlockIdAtom = atom<string | null>(null);
|
||||||
|
export const hasAnimationPlayedAtom = atom<boolean | null>(true);
|
||||||
|
|
||||||
previewBlockIdAtom.onMount = set => {
|
previewBlockIdAtom.onMount = set => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/// <reference types="react/experimental" />
|
/// <reference types="react/experimental" />
|
||||||
import '@blocksuite/blocks';
|
import '@blocksuite/blocks';
|
||||||
|
|
||||||
|
import { Button, Tooltip } from '@affine/component';
|
||||||
import type { ImageBlockModel } from '@blocksuite/blocks';
|
import type { ImageBlockModel } from '@blocksuite/blocks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import {
|
import {
|
||||||
@@ -21,24 +22,26 @@ import { Suspense, useCallback } from 'react';
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import Button from '../../ui/button/button';
|
|
||||||
import { useZoomControls } from './hooks/use-zoom';
|
import { useZoomControls } from './hooks/use-zoom';
|
||||||
import {
|
import {
|
||||||
|
buttonIconStyle,
|
||||||
buttonStyle,
|
buttonStyle,
|
||||||
captionStyle,
|
captionStyle,
|
||||||
groupStyle,
|
groupStyle,
|
||||||
imageBottomContainerStyle,
|
imageBottomContainerStyle,
|
||||||
imageNavigationControlStyle,
|
|
||||||
imagePreviewActionBarStyle,
|
imagePreviewActionBarStyle,
|
||||||
|
imagePreviewBackgroundStyle,
|
||||||
imagePreviewModalCaptionStyle,
|
imagePreviewModalCaptionStyle,
|
||||||
imagePreviewModalCenterStyle,
|
imagePreviewModalCenterStyle,
|
||||||
imagePreviewModalCloseButtonStyle,
|
imagePreviewModalCloseButtonStyle,
|
||||||
imagePreviewModalContainerStyle,
|
imagePreviewModalContainerStyle,
|
||||||
imagePreviewModalGoStyle,
|
|
||||||
imagePreviewModalStyle,
|
imagePreviewModalStyle,
|
||||||
scaleIndicatorStyle,
|
loaded,
|
||||||
|
scaleIndicatorButtonStyle,
|
||||||
|
unloaded,
|
||||||
} from './index.css';
|
} from './index.css';
|
||||||
import { previewBlockIdAtom } from './index.jotai';
|
import { hasAnimationPlayedAtom, previewBlockIdAtom } from './index.jotai';
|
||||||
|
import { toast } from './toast';
|
||||||
|
|
||||||
export type ImagePreviewModalProps = {
|
export type ImagePreviewModalProps = {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
@@ -52,64 +55,46 @@ const ImagePreviewModalImpl = (
|
|||||||
}
|
}
|
||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
const [blockId, setBlockId] = useAtom(previewBlockIdAtom);
|
const [blockId, setBlockId] = useAtom(previewBlockIdAtom);
|
||||||
|
|
||||||
const [bIsActionBarVisible, setBIsActionBarVisible] = useState(false);
|
|
||||||
const [caption, setCaption] = useState(() => {
|
|
||||||
const page = props.workspace.getPage(props.pageId);
|
|
||||||
assertExists(page);
|
|
||||||
const block = page.getBlockById(props.blockId) as ImageBlockModel;
|
|
||||||
assertExists(block);
|
|
||||||
return block?.caption;
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
const page = props.workspace.getPage(props.pageId);
|
|
||||||
assertExists(page);
|
|
||||||
const block = page.getBlockById(props.blockId) as ImageBlockModel;
|
|
||||||
assertExists(block);
|
|
||||||
setCaption(block?.caption);
|
|
||||||
}, [props.blockId, props.pageId, props.workspace]);
|
|
||||||
const { data } = useSWR(['workspace', 'embed', props.pageId, props.blockId], {
|
|
||||||
fetcher: ([_, __, pageId, blockId]) => {
|
|
||||||
const page = props.workspace.getPage(pageId);
|
|
||||||
assertExists(page);
|
|
||||||
const block = page.getBlockById(blockId) as ImageBlockModel;
|
|
||||||
assertExists(block);
|
|
||||||
return props.workspace.blobs.get(block?.sourceId);
|
|
||||||
},
|
|
||||||
suspense: true,
|
|
||||||
});
|
|
||||||
const zoomRef = useRef<HTMLDivElement | null>(null);
|
const zoomRef = useRef<HTMLDivElement | null>(null);
|
||||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||||
const {
|
const {
|
||||||
zoomIn,
|
|
||||||
zoomOut,
|
|
||||||
isZoomedBigger,
|
isZoomedBigger,
|
||||||
handleDrag,
|
handleDrag,
|
||||||
handleDragStart,
|
handleDragStart,
|
||||||
handleDragEnd,
|
handleDragEnd,
|
||||||
resetZoom,
|
resetZoom,
|
||||||
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
resetScale,
|
||||||
currentScale,
|
currentScale,
|
||||||
} = useZoomControls({ zoomRef, imageRef });
|
} = useZoomControls({ zoomRef, imageRef });
|
||||||
const [prevData, setPrevData] = useState<string | null>(() => data);
|
const [isOpen, setIsOpen] = useAtom(hasAnimationPlayedAtom);
|
||||||
const [url, setUrl] = useState<string | null>(null);
|
const [hasPlayedAnimation, setHasPlayedAnimation] = useState<boolean>(false);
|
||||||
if (prevData !== data) {
|
|
||||||
if (url) {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
setUrl(URL.createObjectURL(data));
|
|
||||||
|
|
||||||
setPrevData(data);
|
useEffect(() => {
|
||||||
} else if (!url) {
|
let timeoutId: NodeJS.Timeout;
|
||||||
setUrl(URL.createObjectURL(data));
|
|
||||||
}
|
if (!isOpen) {
|
||||||
if (!url) {
|
timeoutId = setTimeout(() => {
|
||||||
return null;
|
props.onClose();
|
||||||
|
setIsOpen(true);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextImageHandler = (blockId: string | null) => {
|
return () => {};
|
||||||
|
}, [isOpen, props, setIsOpen]);
|
||||||
|
|
||||||
|
const nextImageHandler = useCallback(
|
||||||
|
(blockId: string | null) => {
|
||||||
assertExists(blockId);
|
assertExists(blockId);
|
||||||
const workspace = props.workspace;
|
const workspace = props.workspace;
|
||||||
|
if (!hasPlayedAnimation) {
|
||||||
|
setHasPlayedAnimation(true);
|
||||||
|
}
|
||||||
const page = workspace.getPage(props.pageId);
|
const page = workspace.getPage(props.pageId);
|
||||||
assertExists(page);
|
assertExists(page);
|
||||||
const block = page.getBlockById(blockId);
|
const block = page.getBlockById(blockId);
|
||||||
@@ -117,14 +102,17 @@ const ImagePreviewModalImpl = (
|
|||||||
const nextBlock = page
|
const nextBlock = page
|
||||||
.getNextSiblings(block)
|
.getNextSiblings(block)
|
||||||
.find(
|
.find(
|
||||||
(block): block is ImageBlockModel => block.flavour === 'affine:image'
|
(block): block is ImageBlockModel => block.flavour === 'affine:embed'
|
||||||
);
|
);
|
||||||
if (nextBlock) {
|
if (nextBlock) {
|
||||||
setBlockId(nextBlock.id);
|
setBlockId(nextBlock.id);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[props.pageId, props.workspace, setBlockId, hasPlayedAnimation]
|
||||||
|
);
|
||||||
|
|
||||||
const previousImageHandler = (blockId: string | null) => {
|
const previousImageHandler = useCallback(
|
||||||
|
(blockId: string | null) => {
|
||||||
assertExists(blockId);
|
assertExists(blockId);
|
||||||
const workspace = props.workspace;
|
const workspace = props.workspace;
|
||||||
const page = workspace.getPage(props.pageId);
|
const page = workspace.getPage(props.pageId);
|
||||||
@@ -134,17 +122,21 @@ const ImagePreviewModalImpl = (
|
|||||||
const prevBlock = page
|
const prevBlock = page
|
||||||
.getPreviousSiblings(block)
|
.getPreviousSiblings(block)
|
||||||
.findLast(
|
.findLast(
|
||||||
(block): block is ImageBlockModel => block.flavour === 'affine:image'
|
(block): block is ImageBlockModel => block.flavour === 'affine:embed'
|
||||||
);
|
);
|
||||||
if (prevBlock) {
|
if (prevBlock) {
|
||||||
setBlockId(prevBlock.id);
|
setBlockId(prevBlock.id);
|
||||||
}
|
}
|
||||||
};
|
resetZoom();
|
||||||
|
},
|
||||||
|
[props.pageId, props.workspace, setBlockId, resetZoom]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteHandler = (blockId: string) => {
|
const deleteHandler = useCallback(
|
||||||
const workspace = props.workspace;
|
(blockId: string) => {
|
||||||
|
const { pageId, workspace, onClose } = props;
|
||||||
|
|
||||||
const page = workspace.getPage(props.pageId);
|
const page = workspace.getPage(pageId);
|
||||||
assertExists(page);
|
assertExists(page);
|
||||||
const block = page.getBlockById(blockId);
|
const block = page.getBlockById(blockId);
|
||||||
assertExists(block);
|
assertExists(block);
|
||||||
@@ -152,13 +144,15 @@ const ImagePreviewModalImpl = (
|
|||||||
page
|
page
|
||||||
.getPreviousSiblings(block)
|
.getPreviousSiblings(block)
|
||||||
.findLast(
|
.findLast(
|
||||||
(block): block is ImageBlockModel => block.flavour === 'affine:image'
|
(block): block is ImageBlockModel =>
|
||||||
|
block.flavour === 'affine:embed'
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const prevBlock = page
|
const prevBlock = page
|
||||||
.getPreviousSiblings(block)
|
.getPreviousSiblings(block)
|
||||||
.findLast(
|
.findLast(
|
||||||
(block): block is ImageBlockModel => block.flavour === 'affine:image'
|
(block): block is ImageBlockModel =>
|
||||||
|
block.flavour === 'affine:embed'
|
||||||
);
|
);
|
||||||
if (prevBlock) {
|
if (prevBlock) {
|
||||||
setBlockId(prevBlock.id);
|
setBlockId(prevBlock.id);
|
||||||
@@ -167,32 +161,29 @@ const ImagePreviewModalImpl = (
|
|||||||
page
|
page
|
||||||
.getNextSiblings(block)
|
.getNextSiblings(block)
|
||||||
.find(
|
.find(
|
||||||
(block): block is ImageBlockModel => block.flavour === 'affine:image'
|
(block): block is ImageBlockModel =>
|
||||||
|
block.flavour === 'affine:embed'
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const nextBlock = page
|
const nextBlock = page
|
||||||
.getNextSiblings(block)
|
.getNextSiblings(block)
|
||||||
.find(
|
.find(
|
||||||
(block): block is ImageBlockModel => block.flavour === 'affine:image'
|
(block): block is ImageBlockModel =>
|
||||||
|
block.flavour === 'affine:embed'
|
||||||
);
|
);
|
||||||
if (nextBlock) {
|
if (nextBlock) {
|
||||||
const image = imageRef.current;
|
|
||||||
resetZoom();
|
|
||||||
if (image) {
|
|
||||||
image.style.width = '100%'; // Reset the width to its original size
|
|
||||||
image.style.height = 'auto'; // Reset the height to maintain aspect ratio
|
|
||||||
}
|
|
||||||
setBlockId(nextBlock.id);
|
setBlockId(nextBlock.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
props.onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
page.deleteBlock(block);
|
page.deleteBlock(block);
|
||||||
};
|
},
|
||||||
|
[props, setBlockId]
|
||||||
|
);
|
||||||
|
|
||||||
let actionbarTimeout: NodeJS.Timeout;
|
const downloadHandler = useCallback(
|
||||||
|
async (blockId: string | null) => {
|
||||||
const downloadHandler = async (blockId: string | null) => {
|
|
||||||
const workspace = props.workspace;
|
const workspace = props.workspace;
|
||||||
const page = workspace.getPage(props.pageId);
|
const page = workspace.getPage(props.pageId);
|
||||||
assertExists(page);
|
assertExists(page);
|
||||||
@@ -238,66 +229,65 @@ const ImagePreviewModalImpl = (
|
|||||||
new Blob([arrayBuffer], { type: fileType })
|
new Blob([arrayBuffer], { type: fileType })
|
||||||
);
|
);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
const event = new MouseEvent('click');
|
|
||||||
a.download = block.id;
|
|
||||||
a.href = downloadUrl;
|
a.href = downloadUrl;
|
||||||
a.dispatchEvent(event);
|
a.download = block.id ?? 'image';
|
||||||
|
document.body.appendChild(a);
|
||||||
// cleanup
|
a.click();
|
||||||
a.remove();
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[props.pageId, props.workspace]
|
||||||
|
);
|
||||||
|
const [caption, setCaption] = useState(() => {
|
||||||
|
const page = props.workspace.getPage(props.pageId);
|
||||||
|
assertExists(page);
|
||||||
|
const block = page.getBlockById(props.blockId) as ImageBlockModel;
|
||||||
|
assertExists(block);
|
||||||
|
return block?.caption;
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const page = props.workspace.getPage(props.pageId);
|
||||||
|
assertExists(page);
|
||||||
|
const block = page.getBlockById(props.blockId) as ImageBlockModel;
|
||||||
|
assertExists(block);
|
||||||
|
setCaption(block?.caption);
|
||||||
|
}, [props.blockId, props.pageId, props.workspace]);
|
||||||
|
const { data } = useSWR(['workspace', 'embed', props.pageId, props.blockId], {
|
||||||
|
fetcher: ([_, __, pageId, blockId]) => {
|
||||||
|
const page = props.workspace.getPage(pageId);
|
||||||
|
assertExists(page);
|
||||||
|
const block = page.getBlockById(blockId) as ImageBlockModel;
|
||||||
|
assertExists(block);
|
||||||
|
return props.workspace.blobs.get(block?.sourceId);
|
||||||
|
},
|
||||||
|
suspense: true,
|
||||||
|
});
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const [prevData, setPrevData] = useState<string | null>(() => data);
|
||||||
clearTimeout(actionbarTimeout);
|
const [url, setUrl] = useState<string | null>(null);
|
||||||
setBIsActionBarVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
if (prevData !== data) {
|
||||||
actionbarTimeout = setTimeout(() => {
|
if (url) {
|
||||||
setBIsActionBarVisible(false);
|
URL.revokeObjectURL(url);
|
||||||
}, 3000);
|
}
|
||||||
};
|
setUrl(URL.createObjectURL(data));
|
||||||
|
|
||||||
|
setPrevData(data);
|
||||||
|
} else if (!url) {
|
||||||
|
setUrl(URL.createObjectURL(data));
|
||||||
|
}
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="image-preview-modal"
|
|
||||||
className={imagePreviewModalStyle}
|
className={imagePreviewModalStyle}
|
||||||
onClick={event =>
|
onClick={event => {
|
||||||
event.target === event.currentTarget ? props.onClose() : null
|
if (event.target === event.currentTarget) {
|
||||||
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
>
|
|
||||||
{!isZoomedBigger ? (
|
|
||||||
<div className={imageNavigationControlStyle}>
|
|
||||||
<span
|
|
||||||
className={imagePreviewModalGoStyle}
|
|
||||||
style={{
|
|
||||||
left: 0,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
assertExists(blockId);
|
|
||||||
previousImageHandler(blockId);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
❮
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={imagePreviewModalGoStyle}
|
|
||||||
style={{
|
|
||||||
right: 0,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
assertExists(blockId);
|
|
||||||
nextImageHandler(blockId);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
❯
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<div className={imagePreviewModalContainerStyle}>
|
<div className={imagePreviewModalContainerStyle}>
|
||||||
<div
|
<div
|
||||||
className={clsx('zoom-area', { 'zoomed-bigger': isZoomedBigger })}
|
className={clsx('zoom-area', { 'zoomed-bigger': isZoomedBigger })}
|
||||||
@@ -306,6 +296,7 @@ const ImagePreviewModalImpl = (
|
|||||||
<div className={imagePreviewModalCenterStyle}>
|
<div className={imagePreviewModalCenterStyle}>
|
||||||
<img
|
<img
|
||||||
data-blob-id={props.blockId}
|
data-blob-id={props.blockId}
|
||||||
|
data-testid="image-content"
|
||||||
src={url}
|
src={url}
|
||||||
alt={caption}
|
alt={caption}
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
@@ -313,8 +304,6 @@ const ImagePreviewModalImpl = (
|
|||||||
onMouseDown={handleDragStart}
|
onMouseDown={handleDragStart}
|
||||||
onMouseMove={handleDrag}
|
onMouseMove={handleDrag}
|
||||||
onMouseUp={handleDragEnd}
|
onMouseUp={handleDragEnd}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onLoad={resetZoom}
|
onLoad={resetZoom}
|
||||||
/>
|
/>
|
||||||
{isZoomedBigger ? null : (
|
{isZoomedBigger ? null : (
|
||||||
@@ -323,32 +312,9 @@ const ImagePreviewModalImpl = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
props.onClose();
|
|
||||||
}}
|
|
||||||
className={imagePreviewModalCloseButtonStyle}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="10"
|
|
||||||
height="10"
|
|
||||||
viewBox="0 0 10 10"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M0.286086 0.285964C0.530163 0.0418858 0.925891 0.0418858 1.16997 0.285964L5.00013 4.11613L8.83029 0.285964C9.07437 0.0418858 9.4701 0.0418858 9.71418 0.285964C9.95825 0.530041 9.95825 0.925769 9.71418 1.16985L5.88401 5.00001L9.71418 8.83017C9.95825 9.07425 9.95825 9.46998 9.71418 9.71405C9.4701 9.95813 9.07437 9.95813 8.83029 9.71405L5.00013 5.88389L1.16997 9.71405C0.925891 9.95813 0.530163 9.95813 0.286086 9.71405C0.0420079 9.46998 0.0420079 9.07425 0.286086 8.83017L4.11625 5.00001L0.286086 1.16985C0.0420079 0.925769 0.0420079 0.530041 0.286086 0.285964Z"
|
|
||||||
fill="#77757D"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{bIsActionBarVisible ? (
|
|
||||||
<div
|
<div
|
||||||
className={imageBottomContainerStyle}
|
className={imageBottomContainerStyle}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onClick={event => event.stopPropagation()}
|
onClick={event => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
{isZoomedBigger && caption !== '' ? (
|
{isZoomedBigger && caption !== '' ? (
|
||||||
@@ -356,53 +322,81 @@ const ImagePreviewModalImpl = (
|
|||||||
) : null}
|
) : null}
|
||||||
<div className={imagePreviewActionBarStyle}>
|
<div className={imagePreviewActionBarStyle}>
|
||||||
<div>
|
<div>
|
||||||
|
<Tooltip content={'Previous'} disablePortal={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ArrowLeftSmallIcon />}
|
icon={<ArrowLeftSmallIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
assertExists(blockId);
|
assertExists(blockId);
|
||||||
previousImageHandler(blockId);
|
previousImageHandler(blockId);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={'Next'} disablePortal={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ArrowRightSmallIcon />}
|
icon={<ArrowRightSmallIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
assertExists(blockId);
|
assertExists(blockId);
|
||||||
nextImageHandler(blockId);
|
nextImageHandler(blockId);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={groupStyle}>
|
<div className={groupStyle}></div>
|
||||||
|
<Tooltip
|
||||||
|
content={'Fit to Screen'}
|
||||||
|
disablePortal={true}
|
||||||
|
showArrow={false}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<ViewBarIcon />}
|
icon={<ViewBarIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
onClick={() => resetZoom()}
|
onClick={() => resetZoom()}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={'Zoom out'} disablePortal={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<MinusIcon />}
|
icon={<MinusIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
onClick={zoomOut}
|
onClick={zoomOut}
|
||||||
/>
|
/>
|
||||||
<span className={scaleIndicatorStyle}>{`${(
|
</Tooltip>
|
||||||
currentScale * 100
|
<Tooltip content={'Reset Scale'} disablePortal={false}>
|
||||||
).toFixed(0)}%`}</span>
|
|
||||||
<Button
|
<Button
|
||||||
icon={<PlusIcon />}
|
noBorder={true}
|
||||||
|
size={'middle'}
|
||||||
|
className={scaleIndicatorButtonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
|
onClick={resetScale}
|
||||||
|
>
|
||||||
|
{`${(currentScale * 100).toFixed(0)}%`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={'Zoom in'} disablePortal={false}>
|
||||||
|
<Button
|
||||||
|
icon={<PlusIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
onClick={() => zoomIn()}
|
onClick={() => zoomIn()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Tooltip>
|
||||||
<div className={groupStyle}>
|
<div className={groupStyle}></div>
|
||||||
|
<Tooltip content={'Download'} disablePortal={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<DownloadIcon />}
|
icon={<DownloadIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
assertExists(blockId);
|
assertExists(blockId);
|
||||||
downloadHandler(blockId).catch(err => {
|
downloadHandler(blockId).catch(err => {
|
||||||
@@ -410,10 +404,13 @@ const ImagePreviewModalImpl = (
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={'Copy to clipboard'} disablePortal={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyIcon />}
|
icon={<CopyIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!imageRef.current) {
|
if (!imageRef.current) {
|
||||||
return;
|
return;
|
||||||
@@ -440,28 +437,27 @@ const ImagePreviewModalImpl = (
|
|||||||
URL.revokeObjectURL(dataUrl);
|
URL.revokeObjectURL(dataUrl);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(
|
console.error('Error copying image to clipboard', error);
|
||||||
'Error copying image to clipboard',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
URL.revokeObjectURL(dataUrl);
|
URL.revokeObjectURL(dataUrl);
|
||||||
});
|
});
|
||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
|
toast('Copied to clipboard.');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Tooltip>
|
||||||
<div className={groupStyle}>
|
<div className={groupStyle}></div>
|
||||||
|
<Tooltip content={'Delete'} disablePortal={false}>
|
||||||
<Button
|
<Button
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon className={buttonIconStyle} />}
|
||||||
noBorder={true}
|
noBorder={true}
|
||||||
className={buttonStyle}
|
className={buttonStyle}
|
||||||
onClick={() => blockId && deleteHandler(blockId)}
|
onClick={() => blockId && deleteHandler(blockId)}
|
||||||
|
hoverColor={'-moz-initial'}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -469,13 +465,16 @@ export const ImagePreviewModal = (
|
|||||||
props: ImagePreviewModalProps
|
props: ImagePreviewModalProps
|
||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
const [blockId, setBlockId] = useAtom(previewBlockIdAtom);
|
const [blockId, setBlockId] = useAtom(previewBlockIdAtom);
|
||||||
|
const [isOpen, setIsOpen] = useAtom(hasAnimationPlayedAtom);
|
||||||
|
|
||||||
const handleKeyUp = useCallback(
|
const handleKeyUp = useCallback(
|
||||||
(event: KeyboardEvent) => {
|
(event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setBlockId(null);
|
if (isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,7 +494,7 @@ export const ImagePreviewModal = (
|
|||||||
.getPreviousSiblings(block)
|
.getPreviousSiblings(block)
|
||||||
.findLast(
|
.findLast(
|
||||||
(block): block is ImageBlockModel =>
|
(block): block is ImageBlockModel =>
|
||||||
block.flavour === 'affine:image'
|
block.flavour === 'affine:embed'
|
||||||
);
|
);
|
||||||
if (prevBlock) {
|
if (prevBlock) {
|
||||||
setBlockId(prevBlock.id);
|
setBlockId(prevBlock.id);
|
||||||
@@ -505,7 +504,7 @@ export const ImagePreviewModal = (
|
|||||||
.getNextSiblings(block)
|
.getNextSiblings(block)
|
||||||
.find(
|
.find(
|
||||||
(block): block is ImageBlockModel =>
|
(block): block is ImageBlockModel =>
|
||||||
block.flavour === 'affine:image'
|
block.flavour === 'affine:embed'
|
||||||
);
|
);
|
||||||
if (nextBlock) {
|
if (nextBlock) {
|
||||||
setBlockId(nextBlock.id);
|
setBlockId(nextBlock.id);
|
||||||
@@ -516,7 +515,7 @@ export const ImagePreviewModal = (
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
},
|
},
|
||||||
[blockId, setBlockId, props.workspace, props.pageId]
|
[blockId, setBlockId, props.workspace, props.pageId, isOpen, setIsOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -531,12 +530,38 @@ export const ImagePreviewModal = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div className={imagePreviewModalStyle} />}>
|
<div
|
||||||
|
data-testid="image-preview-modal"
|
||||||
|
className={`${imagePreviewBackgroundStyle} ${isOpen ? loaded : unloaded}`}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<div />}>
|
||||||
<ImagePreviewModalImpl
|
<ImagePreviewModalImpl
|
||||||
{...props}
|
{...props}
|
||||||
blockId={blockId}
|
blockId={blockId}
|
||||||
onClose={() => setBlockId(null)}
|
onClose={() => setBlockId(null)}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className={imagePreviewModalCloseButtonStyle}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M0.286086 0.285964C0.530163 0.0418858 0.925891 0.0418858 1.16997 0.285964L5.00013 4.11613L8.83029 0.285964C9.07437 0.0418858 9.4701 0.0418858 9.71418 0.285964C9.95825 0.530041 9.95825 0.925769 9.71418 1.16985L5.88401 5.00001L9.71418 8.83017C9.95825 9.07425 9.95825 9.46998 9.71418 9.71405C9.4701 9.95813 9.07437 9.95813 8.83029 9.71405L5.00013 5.88389L1.16997 9.71405C0.925891 9.95813 0.530163 9.95813 0.286086 9.71405C0.0420079 9.46998 0.0420079 9.07425 0.286086 8.83017L4.11625 5.00001L0.286086 1.16985C0.0420079 0.925769 0.0420079 0.530041 0.286086 0.285964Z"
|
||||||
|
fill="#77757D"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { ToastOptions } from '@affine/component';
|
||||||
|
import { toast as basicToast } from '@affine/component';
|
||||||
|
|
||||||
|
export const toast = (message: string, options?: ToastOptions) => {
|
||||||
|
const mainContainer = document.querySelector(
|
||||||
|
'[data-testid="image-preview-modal"]'
|
||||||
|
) as HTMLElement;
|
||||||
|
return basicToast(message, {
|
||||||
|
portal: mainContainer || document.body,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// global Events
|
||||||
|
interface WindowEventMap {
|
||||||
|
'affine-toast:emit': CustomEvent<{
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export const toast = (
|
|||||||
duration: 2500,
|
duration: 2500,
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
if (!ToastContainer) {
|
if (!ToastContainer || (portal && !portal.contains(ToastContainer))) {
|
||||||
ToastContainer = createToastContainer(portal);
|
ToastContainer = createToastContainer(portal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +126,7 @@ export const toast = (
|
|||||||
element.style.margin = '0';
|
element.style.margin = '0';
|
||||||
element.style.padding = '0';
|
element.style.padding = '0';
|
||||||
// wait for transition
|
// wait for transition
|
||||||
|
// ToastContainer = null;
|
||||||
element.addEventListener('transitionend', () => {
|
element.addEventListener('transitionend', () => {
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const StyledTooltip = styled(StyledPopperContainer)(() => {
|
|||||||
backgroundColor: 'var(--affine-tooltip)',
|
backgroundColor: 'var(--affine-tooltip)',
|
||||||
color: 'var(--affine-white)',
|
color: 'var(--affine-white)',
|
||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '12px',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user