mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 21:59:00 +02:00
a293474c0c
Add analytics event for context menu, comment, track changes, and jump to location GitOrigin-RevId: 8412cc3c8039cd1582ccee20b162b4bef4467dea
244 lines
7.6 KiB
TypeScript
244 lines
7.6 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 { useProjectContext } from '@/shared/context/project-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, useRef } 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'
|
|
import { useEditorContext } from '@/shared/context/editor-context'
|
|
import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode'
|
|
import {
|
|
sendContextMenuEvent,
|
|
ContextMenuItemSegmentation,
|
|
} from '../utils/context-menu-analytics'
|
|
|
|
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,
|
|
changeLayout,
|
|
pdfLayout,
|
|
view: ideView,
|
|
} = useLayoutContext()
|
|
const visualPreviewEnabled = useFeatureFlag('visual-preview')
|
|
const { t } = useTranslation()
|
|
const { shortcuts } = useCommandRegistry()
|
|
const { features } = useProjectContext()
|
|
const requestedPdfSyncRef = useRef(false)
|
|
const { setUpgradeTrackChangesModal } = useEditorContext()
|
|
const trackingChangesMode = useTrackingChangesMode()
|
|
const isReview = trackingChangesMode === 'review'
|
|
|
|
const closeMenu = useCallback(() => {
|
|
view.dispatch({ effects: closeContextMenuEffect.of(null) })
|
|
view.focus()
|
|
}, [view])
|
|
|
|
// Handle closing the menu when it loses focus, e.g. click outside the editor
|
|
const onToggle = (show: boolean) => {
|
|
if (!show) {
|
|
// Skip closing if a sync to PDF is in flight
|
|
if (requestedPdfSyncRef.current) {
|
|
return
|
|
}
|
|
|
|
closeMenu()
|
|
}
|
|
}
|
|
|
|
// Wait for syncToPdf to finish before closing the menu
|
|
useEffect(() => {
|
|
if (requestedPdfSyncRef.current && !syncToPdfInFlight) {
|
|
closeMenu()
|
|
// Clear the synchronous flag when the close completes
|
|
requestedPdfSyncRef.current = false
|
|
}
|
|
}, [syncToPdfInFlight, closeMenu])
|
|
|
|
const hasSelection = !state.selection.main.empty
|
|
const canEdit = permissions.write || permissions.trackedWrite
|
|
|
|
// Determine layout states for PDF sync functionality
|
|
const isPdfDetached = detachRole === 'detacher'
|
|
const isEditorOnly =
|
|
pdfLayout === 'flat' && ideView === 'editor' && !isPdfDetached
|
|
|
|
const jumpToLocationInPdfEnabled =
|
|
pdfUrl && pdfViewer !== 'native' && !visualPreviewEnabled && canSyncToPdf
|
|
|
|
const wrapForContextMenu = useCallback(
|
|
(
|
|
item: ContextMenuItemSegmentation,
|
|
command: () => Promise<boolean> | boolean
|
|
) =>
|
|
async () => {
|
|
sendContextMenuEvent('menu-click', {
|
|
location: 'editor-context-menu',
|
|
item,
|
|
})
|
|
const result = await command()
|
|
if (result !== false) {
|
|
view.focus()
|
|
closeMenu()
|
|
}
|
|
},
|
|
[view, closeMenu]
|
|
)
|
|
|
|
const inVisualMode = isVisual(view)
|
|
|
|
const handleCut = wrapForContextMenu('cut', () => cutSelection(view))
|
|
const handleCopy = wrapForContextMenu('copy', () => copySelection(view))
|
|
const handlePaste = wrapForContextMenu('paste', () =>
|
|
inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view)
|
|
)
|
|
const handlePasteSpecial = wrapForContextMenu(
|
|
inVisualMode ? 'paste-without-formatting' : 'paste-with-formatting',
|
|
() =>
|
|
inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
|
|
)
|
|
const handleDelete = wrapForContextMenu('delete', () =>
|
|
commands.deleteSelection(view)
|
|
)
|
|
|
|
const handleToggleTrackChanges = wrapForContextMenu(
|
|
wantTrackChanges ? 'back-to-editing' : 'suggest-edits',
|
|
() => {
|
|
// Matching the logic in review toggle to ensure consistency for server pro
|
|
if (!features.trackChanges && !isReview) {
|
|
setUpgradeTrackChangesModal({
|
|
show: true,
|
|
location: 'editor-context-menu',
|
|
})
|
|
return true
|
|
}
|
|
window.dispatchEvent(new Event('toggle-track-changes'))
|
|
return true
|
|
}
|
|
)
|
|
|
|
const handleComment = wrapForContextMenu('comment', () => {
|
|
commands.addComment('editor-context-menu')
|
|
return true
|
|
})
|
|
|
|
// Sync-to-PDF is special: it needs to wait for async completion before closing
|
|
const handleSyncToPdf = useCallback(() => {
|
|
// Switch to split view only when in editor-only mode with non-detached PDF
|
|
if (isEditorOnly) {
|
|
changeLayout('sideBySide')
|
|
}
|
|
|
|
sendContextMenuEvent('menu-click', {
|
|
location: 'editor-context-menu',
|
|
item: 'jump-to-location-in-pdf',
|
|
})
|
|
sendContextMenuEvent('jump-to-location', {
|
|
method: 'editor-context-menu',
|
|
direction: 'code-location-in-pdf',
|
|
})
|
|
requestedPdfSyncRef.current = true
|
|
syncToPdf()
|
|
view.focus()
|
|
}, [syncToPdf, view, changeLayout, isEditorOnly])
|
|
|
|
const getShortcut = useCallback(
|
|
(id: string) => {
|
|
const shortcut = shortcuts[id]?.[0]
|
|
return shortcut ? formatShortcut(shortcut) : undefined
|
|
},
|
|
[shortcuts]
|
|
)
|
|
|
|
return {
|
|
closeMenu,
|
|
onToggle,
|
|
menuItems: [
|
|
{
|
|
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 && features.trackChangesVisible,
|
|
shortcut: getShortcut('toggle-track-changes'),
|
|
},
|
|
{
|
|
label: t('comment'),
|
|
handler: handleComment,
|
|
disabled: !hasSelection,
|
|
show: permissions.comment,
|
|
shortcut: getShortcut('insert-comment'),
|
|
},
|
|
].filter(item => item.show),
|
|
}
|
|
}
|