import { ElementType, memo, useCallback, useEffect, useRef, useState, } from 'react' import { createPortal } from 'react-dom' import { useCodeMirrorStateContext, useCodeMirrorViewContext, } from './codemirror-context' import { useResizeObserver } from '@/shared/hooks/use-resize-observer' import { ToolbarItems } from './toolbar/toolbar-items' import { ToolbarOverflow } from './toolbar/overflow' import useDropdown from '../../../shared/hooks/use-dropdown' import { getPanel } from '@codemirror/view' import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel' import EditorSwitch from './editor-switch' import SwitchToPDFButton from './switch-to-pdf-button' import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control' import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper' import { isVisual } from '../extensions/visual/visual' import { language } from '@codemirror/language' import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors' import { debugConsole } from '@/utils/debugging' import { useTranslation } from 'react-i18next' import { ToggleSearchButton } from '@/features/source-editor/components/toolbar/toggle-search-button' import ReviewPanelHeader from '@/features/review-panel/components/review-panel-header' import useReviewPanelLayout from '@/features/review-panel/hooks/use-review-panel-layout' import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' import Breadcrumbs from '@/features/source-editor/extensions/breadcrumbs' import classNames from 'classnames' import { useUserSettingsContext } from '@/shared/context/user-settings-context' import { useFeatureFlag } from '@/shared/context/split-test-context' import importOverleafModules from '../../../../macros/import-overleaf-module.macro' const sourceEditorToolbarComponents = importOverleafModules( 'sourceEditorToolbarComponents' ) as { import: { default: ElementType }; path: string }[] const sourceEditorToolbarEndButtons = importOverleafModules( 'sourceEditorToolbarEndButtons' ) as { import: { default: ElementType }; path: string }[] export const CodeMirrorToolbar = () => { const view = useCodeMirrorViewContext() const panel = getPanel(view, createToolbarPanel) if (!panel) { return null } return createPortal(, panel.dom) } const Toolbar = memo(function Toolbar() { const { t } = useTranslation() const state = useCodeMirrorStateContext() const view = useCodeMirrorViewContext() const { userSettings: { breadcrumbs }, } = useUserSettingsContext() const visualPreviewEnabled = useFeatureFlag('visual-preview') const [overflowed, setOverflowed] = useState(false) const overflowedItemsRef = useRef>(new Set()) const languageName = state.facet(language)?.name const visual = isVisual(view) const listDepth = minimumListDepthForSelection(state) const newEditor = useIsNewEditorEnabled() const { showHeader: showReviewPanelHeader } = useReviewPanelLayout() const { open: overflowOpen, onToggle: setOverflowOpen, ref: overflowRef, } = useDropdown() const buildOverflow = useCallback( (element: Element) => { debugConsole.log('recalculating toolbar overflow') setOverflowOpen(false) setOverflowed(true) overflowedItemsRef.current = new Set() const buttonGroups = [ ...element.querySelectorAll('[data-overflow]'), ].reverse() // restore all the overflowed items for (const buttonGroup of buttonGroups) { buttonGroup.classList.remove('overflow-hidden') } // find all the available items for (const buttonGroup of buttonGroups) { if (element.scrollWidth <= element.clientWidth) { break } // add this item to the overflow overflowedItemsRef.current.add(buttonGroup.dataset.overflow!) buttonGroup.classList.add('overflow-hidden') } setOverflowed(overflowedItemsRef.current.size > 0) }, [setOverflowOpen] ) // calculate overflow when the container resizes const { elementRef, resizeRef } = useResizeObserver(buildOverflow) // calculate overflow when `languageName` or `visual` change useEffect(() => { if (resizeRef.current) { buildOverflow(resizeRef.current.element) } }, [buildOverflow, languageName, resizeRef, visual]) // calculate overflow when buttons change const observerRef = useRef(null) const handleButtons = useCallback( (node: HTMLDivElement) => { if (!('MutationObserver' in window)) { return } if (node) { observerRef.current = new MutationObserver(() => { if (resizeRef.current) { buildOverflow(resizeRef.current.element) } }) observerRef.current.observe(node, { childList: true }) } else if (observerRef.current) { observerRef.current.disconnect() } }, [buildOverflow, resizeRef] ) // calculate overflow when active element changes to/from inside a table const insideTable = document.activeElement?.closest( '.table-generator-help-modal,.table-generator,.table-generator-width-modal' ) useEffect(() => { if (resizeRef.current) { buildOverflow(resizeRef.current.element) } }, [buildOverflow, insideTable, resizeRef]) const showActions = !state.readOnly && !insideTable return ( <> {newEditor && showReviewPanelHeader && } {!visualPreviewEnabled && } {showActions && ( )} {showActions && ( )} {sourceEditorToolbarEndButtons.map( ({ import: { default: Component }, path }) => ( ) )} {sourceEditorToolbarComponents.map( ({ import: { default: Component }, path }) => ( ) )} {newEditor && breadcrumbs && } > ) })