Просмотр исходного кода

Merge pull request #13142 from overleaf/mj-figure-modal-paste

[cm6] Add image paste handler to rich text

GitOrigin-RevId: 3c814bf64438b387b7b08b8bf89b917347371492
Mathias Jakobsen 3 лет назад
Родитель
Сommit
a06118b96d

+ 3 - 0
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<FigureModalState>
@@ -55,6 +57,7 @@ const reducer = (prev: FigureModalState, action: Partial<FigureModalState>) => {
       sourcePickerShown: false,
       getPath: undefined,
       error: undefined,
+      pastedImageData: undefined,
       ...action,
     }
   }

+ 19 - 9
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<FigureData>(editFigureData, false)
       if (!figure) {
         return
@@ -106,14 +109,21 @@ const FigureModalContent = () => {
         includeCaption: figure.caption !== null,
         includeLabel: figure.label !== null,
       })
-    }
-
-    window.addEventListener('figure-modal:open-modal', listener)
+    }, [view, dispatch, updateExistingFigure])
+  )
 
-    return () => {
-      window.removeEventListener('figure-modal:open-modal', listener)
-    }
-  }, [view, dispatch, updateExistingFigure])
+  useEventListener(
+    'figure-modal:paste-image',
+    useCallback(
+      (image: CustomEvent<PastedImageData>) => {
+        dispatch({
+          source: FigureModalSource.FILE_UPLOAD,
+          pastedImageData: image.detail,
+        })
+      },
+      [dispatch]
+    )
+  )
 
   const insert = useCallback(async () => {
     const figure = view.state.field<FigureData>(editFigureData, false)

+ 7 - 1
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<File | null>(null)
@@ -188,6 +188,12 @@ export const FigureModalUploadFileSource: FC = () => {
     dispatch,
   ])
 
+  useEffect(() => {
+    if (pastedImageData) {
+      uppy.addFile(pastedImageData)
+    }
+  }, [uppy, pastedImageData])
+
   return (
     <>
       <div className="figure-modal-upload">

+ 41 - 0
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<T> = {
   readonly [P in keyof T]: NestedReadonly<T[P]>
@@ -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<PastedImageData>('figure-modal:paste-image', {
+          detail: {
+            name: file.name,
+            type: file.type,
+            data: file,
+          },
+        })
+      )
+    },
+  })
+}

+ 2 - 0
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(),
 ]