Merge pull request #35182 from overleaf/ii-use-event-listener

[web] Replace useScopeEventEmitter and useScopeEventListener with useEventListener

GitOrigin-RevId: 8348c8c980b01e172fee3371980176d2b58ceec6
This commit is contained in:
ilkin-overleaf
2026-09-17 11:22:34 +00:00
committed by Copybot
parent df4e336399
commit 6f8ecc7678
14 changed files with 45 additions and 188 deletions
@@ -24,7 +24,6 @@ import {
findDocEntityById,
findFileRefEntityById,
} from '@/features/ide-react/util/find-doc-entity-by-id'
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { useTranslation } from 'react-i18next'
import customLocalStorage from '@/infrastructure/local-storage'
@@ -111,8 +110,6 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
wantTrackChangesRef.current = wantTrackChanges
}, [wantTrackChanges])
const goToLineEmitter = useScopeEventEmitter('editor:gotoLine')
const { fileTreeData } = useFileTreeData()
const [ignoringExternalUpdates, setIgnoringExternalUpdates] = useState(false)
@@ -197,12 +194,11 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
[currentDocumentId]
)
const jumpToLine = useCallback(
(options: GotoLineOptions) => {
goToLineEmitter(options)
},
[goToLineEmitter]
)
const jumpToLine = useCallback((options: GotoLineOptions) => {
window.dispatchEvent(
new CustomEvent('editor:gotoLine', { detail: options })
)
}, [])
const attachErrorHandlerToDocument = useCallback(
(doc: Doc, document: DocumentContainer) => {
@@ -417,7 +413,9 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
}
} else if (hasGotoOffset(options)) {
const jump = () => {
eventEmitter.emit('editor:gotoOffset', options)
window.dispatchEvent(
new CustomEvent('editor:gotoOffset', { detail: options })
)
}
if (isNewDoc) {
@@ -16,7 +16,6 @@ import {
import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import { postJSON } from '@/infrastructure/fetch-json'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
import getMeta from '@/utils/meta'
import { type PermissionsLevel } from '@/features/ide-react/types/permissions'
import { useProjectContext } from '@/shared/context/project-context'
@@ -48,9 +47,6 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
const [permissionsLevel, setPermissionsLevel] =
useState<PermissionsLevel>('readOnly')
const [outOfSync, setOutOfSync] = useState(false)
const [scopeEventEmitter] = useState(
() => new ReactScopeEventEmitter(eventEmitter)
)
const [unstableStore] = useState(() => {
const store = new ReactScopeValueStore()
// Add dummy editor.ready key for Writefull, that relies on this calling
@@ -165,11 +161,7 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
return (
<IdeReactContext.Provider value={value}>
<IdeProvider
ide={ide}
scopeEventEmitter={scopeEventEmitter}
unstableStore={unstableStore}
>
<IdeProvider ide={ide} unstableStore={unstableStore}>
{children}
</IdeProvider>
</IdeReactContext.Provider>
@@ -15,8 +15,8 @@ import { Doc } from '../../../../../types/doc'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { findDocEntityById } from '@/features/ide-react/util/find-doc-entity-by-id'
import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import useEventListener from '@/shared/hooks/use-event-listener'
import { debugConsole } from '@/utils/debugging'
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
import { getHueForUserId } from '@/shared/utils/colors'
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
@@ -199,21 +199,15 @@ export const OnlineUsersProvider: FC<React.PropsWithChildren> = ({
}, [eventEmitter, setAllValues, setOnlineUsers, socket])
// Track the position of the main cursor
useEffect(() => {
const handleCursorUpdate = ({
detail: [position],
}: CustomEvent<IdeEvents['cursor:editor:update']>) => {
useEventListener(
'cursor:editor:update',
useCallback((event: CustomEvent<CursorPosition>) => {
const position = event.detail
if (position) {
setCurrentPosition(position)
}
}
eventEmitter.on('cursor:editor:update', handleCursorUpdate)
return () => {
eventEmitter.off('cursor:editor:update', handleCursorUpdate)
}
}, [cursorUpdateInterval, eventEmitter])
}, [])
)
// Send the latest position to other clients when currentPosition changes
useEffect(() => {
@@ -8,7 +8,6 @@ import {
useMemo,
useState,
} from 'react'
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
import useEventListener from '@/shared/hooks/use-event-listener'
import { isValidTeXFile } from '@/main/is-valid-tex-file'
import localStorage from '@/infrastructure/local-storage'
@@ -55,8 +54,6 @@ export const OutlineProvider: FC<React.PropsWithChildren> = ({ children }) => {
const [ignoreNextScroll, setIgnoreNextScroll] = useState<boolean>(false)
const { sendEvent } = useEditorAnalytics()
const goToLineEmitter = useScopeEventEmitter('editor:gotoLine', true)
useEventListener(
'file-view:file-opened',
useCallback(() => {
@@ -106,14 +103,18 @@ export const OutlineProvider: FC<React.PropsWithChildren> = ({ children }) => {
const jumpToLine = useCallback(
(lineNumber: number, syncToPdf: boolean) => {
setIgnoreNextScroll(true)
goToLineEmitter({
gotoLine: lineNumber,
gotoColumn: 0,
syncToPdf,
})
window.dispatchEvent(
new CustomEvent('editor:gotoLine', {
detail: {
gotoLine: lineNumber,
gotoColumn: 0,
syncToPdf,
},
})
)
sendEvent('outline-jump-to-line')
},
[goToLineEmitter, sendEvent]
[sendEvent]
)
const highlightedLine = useMemo(
@@ -1,37 +0,0 @@
import {
ScopeEventEmitter,
ScopeEventName,
} from '../../../../../types/ide/scope-event-emitter'
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
export class ReactScopeEventEmitter implements ScopeEventEmitter {
// eslint-disable-next-line no-useless-constructor
constructor(private readonly eventEmitter: EventTarget) {}
emit<T extends ScopeEventName>(
eventName: T,
broadcast: boolean,
...detail: IdeEvents[T]
) {
this.eventEmitter.dispatchEvent(new CustomEvent(eventName, { detail }))
}
on<T extends ScopeEventName>(
eventName: T,
listener: (event: Event, ...args: IdeEvents[T]) => void
) {
const wrappedListener = (event: CustomEvent<IdeEvents[T]>) => {
listener(event, ...event.detail)
}
this.eventEmitter.addEventListener(
eventName,
wrappedListener as EventListener
)
return () => {
this.eventEmitter.removeEventListener(
eventName,
wrappedListener as EventListener
)
}
}
}
@@ -1,7 +1,6 @@
import { FC, memo } from 'react'
import { FC, memo, useCallback } from 'react'
import OutlinePane from '@/features/outline/components/outline-pane'
import { useOutlineContext } from '@/features/ide-react/context/outline-context'
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
import useNestedOutline from '../hooks/use-nested-outline'
export const OutlineContainer: FC = memo(() => {
@@ -13,7 +12,9 @@ export const OutlineContainer: FC = memo(() => {
toggleOutlineExpanded,
} = useOutlineContext()
const outlineToggledEmitter = useScopeEventEmitter('outline-toggled')
const handleToggle = useCallback((isOpen: boolean) => {
window.dispatchEvent(new CustomEvent('outline-toggled', { detail: isOpen }))
}, [])
const outline = useNestedOutline()
@@ -21,7 +22,7 @@ export const OutlineContainer: FC = memo(() => {
<div className="outline-container">
<OutlinePane
outline={outline.items}
onToggle={outlineToggledEmitter}
onToggle={handleToggle}
isTexFile={canShowOutline}
jumpToLine={jumpToLine}
highlightedLine={highlightedLine}
@@ -8,7 +8,6 @@ import useDetachState from '../../../shared/hooks/use-detach-state'
import useDetachAction from '../../../shared/hooks/use-detach-action'
import localStorage from '../../../infrastructure/local-storage'
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
import { debugConsole } from '@/utils/debugging'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
@@ -165,7 +164,7 @@ export default function useSynctex(): {
}
}, [getCurrentFilePath, goToPdfLocation])
useScopeEventListener(
useEventListener(
'cursor:editor:syncToPdf',
useCallback(() => {
syncToPdf()
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useRef } from 'react'
import { EditorState } from '@codemirror/state'
import useScopeEventEmitter from '../../../shared/hooks/use-scope-event-emitter'
import useEventListener from '../../../shared/hooks/use-event-listener'
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
import { createExtensions } from '../extensions'
import { setEditorTheme, setOptionsTheme } from '../extensions/theme'
import {
@@ -504,13 +502,16 @@ function useCodeMirrorScope(view: EditorView) {
settingsRef.current.referencesSearchMode = referencesSearchMode
}, [referencesSearchMode])
const emitSyncToPdf = useScopeEventEmitter('cursor:editor:syncToPdf')
const emitSyncToPdf = useCallback(() => {
window.dispatchEvent(new CustomEvent('cursor:editor:syncToPdf'))
}, [])
// select and scroll to position on editor:gotoLine event (from synctex)
useScopeEventListener(
useEventListener(
'editor:gotoLine',
useCallback(
(_event: any, options: GotoLineOptions) => {
(event: CustomEvent<GotoLineOptions>) => {
const options = event.detail
setCursorLineAndScroll(
view,
options.gotoLine,
@@ -526,42 +527,16 @@ function useCodeMirrorScope(view: EditorView) {
)
// select and scroll to position on editor:gotoOffset event (from review panel)
useScopeEventListener(
useEventListener(
'editor:gotoOffset',
useCallback(
(_event: any, options: GotoOffsetOptions) => {
setCursorPositionAndScroll(view, options.gotoOffset)
(event: CustomEvent<GotoOffsetOptions>) => {
setCursorPositionAndScroll(view, event.detail.gotoOffset)
},
[view]
)
)
// dispatch 'cursor:editor:update' to Angular scope (for synctex and realtime)
const dispatchCursorUpdate = useScopeEventEmitter('cursor:editor:update')
const handleCursorUpdate = useCallback(
(event: CustomEvent) => {
dispatchCursorUpdate(event.detail)
},
[dispatchCursorUpdate]
)
// listen for 'cursor:editor:update' events from CodeMirror, and dispatch them to Angular
useEventListener('cursor:editor:update', handleCursorUpdate)
// dispatch 'cursor:editor:update' to Angular scope (for outline)
const dispatchScrollUpdate = useScopeEventEmitter('scroll:editor:update')
const handleScrollUpdate = useCallback(
(event: CustomEvent) => {
dispatchScrollUpdate(event.detail)
},
[dispatchScrollUpdate]
)
// listen for 'cursor:editor:update' events from CodeMirror, and dispatch them to Angular
useEventListener('scroll:editor:update', handleScrollUpdate)
// enable the compile log linter a) when "Code Check" is off, b) when the project hasn't changed and isn't compiling.
// the project "changed at" date is reset at the start of the compile, i.e. "the project hasn't changed",
// but we don't want to display the compile log diagnostics from the previous compile.
@@ -1,6 +1,5 @@
import { createContext, FC, useContext, useEffect, useMemo } from 'react'
import { ScopeValueStore } from '../../../../types/ide/scope-value-store'
import { ScopeEventEmitter } from '../../../../types/ide/scope-event-emitter'
import { Socket } from '@/features/ide-react/connection/types/socket'
import { useUserSettingsContext } from './user-settings-context'
import { userStyles } from '../utils/styles'
@@ -11,7 +10,6 @@ export type Ide = {
}
type IdeContextValue = Ide & {
scopeEventEmitter: ScopeEventEmitter
unstableStore: ScopeValueStore
}
@@ -20,10 +18,9 @@ export const IdeContext = createContext<IdeContextValue | undefined>(undefined)
export const IdeProvider: FC<
React.PropsWithChildren<{
ide: Ide
scopeEventEmitter: ScopeEventEmitter
unstableStore: ScopeValueStore
}>
> = ({ ide, scopeEventEmitter, unstableStore, children }) => {
> = ({ ide, unstableStore, children }) => {
/**
* Expose unstableStore via `window.overleaf.unstable.store`, so it can be accessed by external extensions.
*
@@ -65,10 +62,9 @@ export const IdeProvider: FC<
const value = useMemo<IdeContextValue>(() => {
return {
...ide,
scopeEventEmitter,
unstableStore,
}
}, [ide, scopeEventEmitter, unstableStore])
}, [ide, unstableStore])
return <IdeContext.Provider value={value}>{children}</IdeContext.Provider>
}
@@ -16,7 +16,6 @@ import getMeta from '../../utils/meta'
import { DetachRole } from './detach-context'
import { debugConsole } from '@/utils/debugging'
import { BinaryFile } from '@/features/file-view/types/binary-file'
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
import useEventListener from '@/shared/hooks/use-event-listener'
import { isMac } from '@/shared/utils/os'
import { sendSearchEvent } from '@/features/event-tracking/search-events'
@@ -90,7 +89,6 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
// what to show in the "flat" view (editor or pdf)
const [view, _setView] = useState<IdeView | null>('editor')
const [openFile, setOpenFile] = useState<BinaryFile | null>(null)
const historyToggleEmitter = useScopeEventEmitter('history:toggle', true)
const { isOpen: railIsOpen, setIsOpen: setRailIsOpen } = useRailContext()
const [prevRailIsOpen, setPrevRailIsOpen] = useState(railIsOpen)
// Whether we came from a file or a document when we left the ide
@@ -102,7 +100,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
_setView(oldValue => {
// ensure that the "history:toggle" event is broadcast when switching in or out of history view
if (value === 'history' || oldValue === 'history') {
historyToggleEmitter()
window.dispatchEvent(new CustomEvent('history:toggle'))
}
if (value === 'history') {
@@ -121,14 +119,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
return value
})
},
[
_setView,
setRailIsOpen,
historyToggleEmitter,
prevRailIsOpen,
setPrevRailIsOpen,
railIsOpen,
]
[_setView, setRailIsOpen, prevRailIsOpen, setPrevRailIsOpen, railIsOpen]
)
const restoreView = useCallback(() => {
@@ -1,18 +0,0 @@
import { useCallback } from 'react'
import { useIdeContext } from '../context/ide-context'
import { ScopeEventName } from '../../../../types/ide/scope-event-emitter'
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
export default function useScopeEventEmitter<T extends ScopeEventName>(
eventName: T,
broadcast = true
) {
const { scopeEventEmitter } = useIdeContext()
return useCallback(
(...detail: IdeEvents[T]) => {
scopeEventEmitter.emit(eventName, broadcast, ...detail)
},
[scopeEventEmitter, eventName, broadcast]
)
}
@@ -1,15 +0,0 @@
import { useEffect } from 'react'
import { useIdeContext } from '../context/ide-context'
import { ScopeEventName } from '../../../../types/ide/scope-event-emitter'
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
export default function useScopeEventListener<T extends ScopeEventName>(
eventName: T,
listener: (event: Event, ...args: IdeEvents[T]) => void
) {
const { scopeEventEmitter } = useIdeContext()
useEffect(() => {
return scopeEventEmitter.on(eventName, listener)
}, [scopeEventEmitter, eventName, listener])
}
@@ -14,7 +14,6 @@ import React, {
import { IdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
import { ConnectionContext } from '@/features/ide-react/context/connection-context'
import {
EditorOpenDocContext,
@@ -398,14 +397,10 @@ const makeIdeReactProvider = (
}))
const [ideContextValue] = useState(() => {
const scopeEventEmitter = new ReactScopeEventEmitter(
new IdeEventEmitter()
)
const unstableStore = new ReactScopeValueStore()
return {
socket,
scopeEventEmitter,
unstableStore,
}
})
@@ -1,15 +0,0 @@
import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
export type ScopeEventName = keyof IdeEvents
export interface ScopeEventEmitter {
emit: <T extends ScopeEventName>(
eventName: T,
broadcast: boolean,
...detail: IdeEvents[T]
) => void
on: <T extends ScopeEventName>(
eventName: T,
listener: (event: Event, ...args: IdeEvents[T]) => void
) => () => void
}