diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-context.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-context.tsx index fc5543c089..e5c93ea888 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-context.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-context.tsx @@ -1,4 +1,5 @@ import { FC, createContext, useContext, useReducer } from 'react' +import { PastedImageData } from '../../extensions/figure-modal' /* eslint-disable no-unused-vars */ export enum FigureModalSource { @@ -20,6 +21,7 @@ type FigureModalState = { includeCaption: boolean includeLabel: boolean error?: string + pastedImageData?: PastedImageData } type FigureModalStateUpdate = Partial @@ -55,6 +57,7 @@ const reducer = (prev: FigureModalState, action: Partial) => { sourcePickerShown: false, getPath: undefined, error: undefined, + pastedImageData: undefined, ...action, } } diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx index 9c0f387a27..146c8fd0df 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx @@ -14,11 +14,13 @@ import { ChangeSpec } from '@codemirror/state' import SplitTestBadge from '../../../../shared/components/split-test-badge' import { FigureData, + PastedImageData, editFigureData, editFigureDataEffect, } from '../../extensions/figure-modal' import { ensureEmptyLine } from '../../extensions/toolbar/commands' import { useTranslation } from 'react-i18next' +import useEventListener from '../../../../shared/hooks/use-event-listener' export const FigureModal = memo(function FigureModal() { return ( @@ -89,8 +91,9 @@ const FigureModalContent = () => { view.focus() }, [dispatch, view]) - useEffect(() => { - const listener = () => { + useEventListener( + 'figure-modal:open-modal', + useCallback(() => { const figure = view.state.field(editFigureData, false) if (!figure) { return @@ -106,14 +109,21 @@ const FigureModalContent = () => { includeCaption: figure.caption !== null, includeLabel: figure.label !== null, }) - } + }, [view, dispatch, updateExistingFigure]) + ) - window.addEventListener('figure-modal:open-modal', listener) - - return () => { - window.removeEventListener('figure-modal:open-modal', listener) - } - }, [view, dispatch, updateExistingFigure]) + useEventListener( + 'figure-modal:paste-image', + useCallback( + (image: CustomEvent) => { + dispatch({ + source: FigureModalSource.FILE_UPLOAD, + pastedImageData: image.detail, + }) + }, + [dispatch] + ) + ) const insert = useCallback(async () => { const figure = view.state.field(editFigureData, false) diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx index ba8784318d..74c10efb50 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx @@ -31,7 +31,7 @@ export enum FileUploadStatus { export const FigureModalUploadFileSource: FC = () => { const { t } = useTranslation() const view = useCodeMirrorViewContext() - const { dispatch } = useFigureModalContext() + const { dispatch, pastedImageData } = useFigureModalContext() const { _id: projectId } = useProjectContext() const [, rootFolder] = useCurrentProjectFolders() const [folder, setFolder] = useState(null) @@ -188,6 +188,12 @@ export const FigureModalUploadFileSource: FC = () => { dispatch, ]) + useEffect(() => { + if (pastedImageData) { + uppy.addFile(pastedImageData) + } + }, [uppy, pastedImageData]) + return ( <>
diff --git a/services/web/frontend/js/features/source-editor/extensions/figure-modal.ts b/services/web/frontend/js/features/source-editor/extensions/figure-modal.ts index 34b8a0105c..68581f1380 100644 --- a/services/web/frontend/js/features/source-editor/extensions/figure-modal.ts +++ b/services/web/frontend/js/features/source-editor/extensions/figure-modal.ts @@ -7,6 +7,7 @@ import { import { EditorView } from '@codemirror/view' import { addEffectListener, removeEffectListener } from './effect-listeners' import { setMetadataEffect } from './language' +import getMeta from '../../../utils/meta' type NestedReadonly = { readonly [P in keyof T]: NestedReadonly @@ -158,3 +159,43 @@ export function waitForFileTreeUpdate(view: EditorView) { promise, } } + +const ALLOWED_MIME_TYPES = new Set([ + 'image/jpeg', + 'image/png', + 'application/pdf', +]) + +export type PastedImageData = { + name: string + type: string + data: Blob +} + +export const figureModalPasteHandler = (): Extension => { + const splitTestVariants = getMeta('ol-splitTestVariants', {}) + const figureModalEnabled = splitTestVariants['figure-modal'] === 'enabled' + if (!figureModalEnabled) { + return [] + } + return EditorView.domEventHandlers({ + paste: evt => { + if (!evt.clipboardData || evt.clipboardData.files.length === 0) { + return + } + const file = evt.clipboardData.files[0] + if (!ALLOWED_MIME_TYPES.has(file.type)) { + return + } + window.dispatchEvent( + new CustomEvent('figure-modal:paste-image', { + detail: { + name: file.name, + type: file.type, + data: file, + }, + }) + ) + }, + }) +} diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index 4d229d4850..50d8985309 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts @@ -21,6 +21,7 @@ import { toolbarPanel } from '../toolbar/toolbar-panel' import { CurrentDoc } from '../../../../../../types/current-doc' import isValidTeXFile from '../../../../main/is-valid-tex-file' import { listItemMarker } from './list-item-marker' +import { figureModalPasteHandler } from '../figure-modal' type Options = { visual: boolean @@ -191,4 +192,5 @@ const extension = (options: Options) => [ toolbarPanel(), scrollJumpAdjuster, showContentWhenParsed, + figureModalPasteHandler(), ]