Files
Verso/services/web/frontend/js/features/source-editor/hooks/use-context-menu-items.tsx
T
Malik GlossopandCopybot a0fc14b367 Merge pull request #30495 from overleaf/mg-context-menu-paste
[web] Support paste with formatting in context menu

GitOrigin-RevId: 551ed1d49ca423395bd9bfc756e10e8d59d71ecd
2026-01-15 14:19:03 +00:00

173 lines
5.2 KiB
TypeScript

import {
useCodeMirrorStateContext,
useCodeMirrorViewContext,
} from '@/features/source-editor/components/codemirror-context'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
import useSynctex from '@/features/pdf-preview/hooks/use-synctex'
import { useDetachCompileContext } from '@/shared/context/detach-compile-context'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect, useState } from 'react'
import {
formatShortcut,
useCommandRegistry,
} from '@/features/ide-react/context/command-registry-context'
import { closeContextMenuEffect } from '../extensions/context-menu'
import * as commands from '../extensions/toolbar/commands'
import {
cutSelection,
copySelection,
pasteWithoutFormatting,
pasteWithFormatting,
} from '../commands/clipboard'
import { isVisual } from '../extensions/visual/visual'
export const useContextMenuItems = () => {
const view = useCodeMirrorViewContext()
const state = useCodeMirrorStateContext()
const permissions = usePermissionsContext()
const { wantTrackChanges } = useEditorPropertiesContext()
const { syncToPdf, syncToPdfInFlight, canSyncToPdf } = useSynctex()
const { pdfUrl, pdfViewer } = useDetachCompileContext()
const { detachRole } = useLayoutContext()
const visualPreviewEnabled = useFeatureFlag('visual-preview')
const { t } = useTranslation()
const { shortcuts } = useCommandRegistry()
const closeMenu = useCallback(() => {
view.dispatch({ effects: closeContextMenuEffect.of(null) })
}, [view])
const [pendingClose, setPendingClose] = useState(false)
// Wait for syncToPdf to finish before closing the menu
useEffect(() => {
if (pendingClose && !syncToPdfInFlight) {
closeMenu()
setPendingClose(false)
}
}, [pendingClose, syncToPdfInFlight, closeMenu])
const hasSelection = !state.selection.main.empty
const canEdit = permissions.write || permissions.trackedWrite
const jumpToLocationInPdfEnabled =
pdfUrl &&
pdfViewer !== 'native' &&
!detachRole &&
!visualPreviewEnabled &&
canSyncToPdf
const wrapForContextMenu = useCallback(
(command: () => Promise<boolean> | boolean) => async () => {
const result = await command()
if (result !== false) {
view.focus()
closeMenu()
}
},
[view, closeMenu]
)
const inVisualMode = isVisual(view)
const handleCut = wrapForContextMenu(() => cutSelection(view))
const handleCopy = wrapForContextMenu(() => copySelection(view))
const handlePaste = wrapForContextMenu(() =>
inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view)
)
const handlePasteSpecial = wrapForContextMenu(() =>
inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
)
const handleDelete = wrapForContextMenu(() => commands.deleteSelection(view))
const handleToggleTrackChanges = wrapForContextMenu(() => {
window.dispatchEvent(new Event('toggle-track-changes'))
return true
})
const handleComment = wrapForContextMenu(() => {
commands.addComment()
return true
})
// Sync-to-PDF is special: it needs to wait for async completion before closing
const handleSyncToPdf = useCallback(() => {
syncToPdf()
setPendingClose(true)
view.focus()
}, [syncToPdf, view])
const getShortcut = useCallback(
(id: string) => {
const shortcut = shortcuts[id]?.[0]
return shortcut ? formatShortcut(shortcut) : undefined
},
[shortcuts]
)
return [
{
label: t('cut'),
handler: handleCut,
disabled: false,
show: canEdit,
shortcut: getShortcut('cut'),
},
{
label: t('copy'),
handler: handleCopy,
disabled: false,
show: true,
shortcut: getShortcut('copy'),
},
{
label: t('paste'),
handler: handlePaste,
disabled: false,
show: canEdit,
shortcut: getShortcut('paste'),
},
{
label: inVisualMode
? t('paste_without_formatting')
: t('paste_with_formatting'),
handler: handlePasteSpecial,
disabled: false,
show: canEdit,
shortcut: inVisualMode ? getShortcut('paste-special') : undefined,
},
{
label: t('delete'),
handler: handleDelete,
disabled: !hasSelection,
show: canEdit,
shortcut: undefined,
},
{
label: t('jump_to_location_in_pdf'),
handler: handleSyncToPdf,
disabled: syncToPdfInFlight,
separatorAbove: true,
show: jumpToLocationInPdfEnabled,
shortcut: undefined,
},
{
label: wantTrackChanges ? t('back_to_editing') : t('suggest_edits'),
handler: handleToggleTrackChanges,
disabled: false,
separatorAbove: true,
show: canEdit,
shortcut: getShortcut('toggle-track-changes'),
},
{
label: t('comment'),
handler: handleComment,
disabled: !hasSelection,
show: permissions.comment,
shortcut: getShortcut('insert-comment'),
},
].filter(item => item.show)
}