diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 196d947149..786478e482 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -342,6 +342,10 @@ "details_provided_by_google_explanation": "", "dictionary": "", "did_you_know_institution_providing_professional": "", + "disable": "", + "disable_equation_preview": "", + "disable_equation_preview_confirm": "", + "disable_equation_preview_enable": "", "disable_single_sign_on": "", "disable_sso": "", "disable_stop_on_first_error": "", @@ -612,6 +616,7 @@ "help_articles_matching": "", "help_improve_overleaf_fill_out_this_survey": "", "help_improve_screen_reader_fill_out_this_survey": "", + "hide": "", "hide_configuration": "", "hide_deleted_user": "", "hide_document_preamble": "", @@ -1047,6 +1052,7 @@ "pending_invite": "", "percent_discount_for_groups": "", "percent_is_the_percentage_of_the_line_width": "", + "permanently_disables_the_preview": "", "personal_library": "", "plan": "", "plan_tooltip": "", @@ -1508,6 +1514,7 @@ "template_description": "", "template_title_taken_from_project_title": "", "templates": "", + "temporarily_hides_the_preview": "", "terminated": "", "test": "", "test_configuration": "", diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx index 2977bf6976..5aaa9e9b95 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx @@ -18,6 +18,7 @@ import { CodeMirrorStateContext, CodeMirrorViewContext, } from './codemirror-context' +import MathPreviewTooltip from './math-preview-tooltip' // TODO: remove this when definitely no longer used export * from './codemirror-context' @@ -39,6 +40,7 @@ function CodeMirrorEditor() { const isMounted = useIsMounted() const newReviewPanel = useFeatureFlag('review-panel-redesign') + const enableMathPreview = useFeatureFlag('math-preview') // create the view using the initial state and intercept transactions const viewRef = useRef(null) @@ -78,6 +80,7 @@ function CodeMirrorEditor() { )} + {enableMathPreview && } {newReviewPanel && } diff --git a/services/web/frontend/js/features/source-editor/components/math-preview-tooltip.tsx b/services/web/frontend/js/features/source-editor/components/math-preview-tooltip.tsx new file mode 100644 index 0000000000..a605b1d94a --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/math-preview-tooltip.tsx @@ -0,0 +1,220 @@ +import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context' +import { + Dropdown, + DropdownMenu, + DropdownToggle, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import MaterialIcon from '@/shared/components/material-icon' +import SplitTestBadge from '@/shared/components/split-test-badge' +import useEventListener from '@/shared/hooks/use-event-listener' +import { FC, useCallback, useEffect, useRef, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { + useCodeMirrorStateContext, + useCodeMirrorViewContext, +} from './codemirror-context' +import { mathPreviewStateField } from '../extensions/math-preview' +import { getTooltip } from '@codemirror/view' +import ReactDOM from 'react-dom' +import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import ControlledDropdown from '@/shared/components/controlled-dropdown' +import { + Dropdown as BS3Dropdown, + MenuItem as BS3MenuItem, +} from 'react-bootstrap' + +const MathPreviewTooltipContainer: FC = () => { + const state = useCodeMirrorStateContext() + const view = useCodeMirrorViewContext() + + const mathPreviewState = state.field(mathPreviewStateField, false) + + if (!mathPreviewState) { + return null + } + + const { tooltip, mathContent } = mathPreviewState + + if (!tooltip || !mathContent) { + return null + } + + const tooltipView = getTooltip(view, tooltip) + + if (!tooltipView) { + return null + } + + return ReactDOM.createPortal( + , + tooltipView.dom + ) +} + +const MathPreviewTooltip: FC<{ mathContent: HTMLDivElement }> = ({ + mathContent, +}) => { + const { t } = useTranslation() + + const [showDisableModal, setShowDisableModal] = useState(false) + const { setMathPreview } = useProjectSettingsContext() + const openDisableModal = useCallback(() => setShowDisableModal(true), []) + const closeDisableModal = useCallback(() => setShowDisableModal(false), []) + + const onHide = useCallback(() => { + window.dispatchEvent(new Event('editor:hideMathTooltip')) + }, []) + + const mathRef = useRef(null) + + const keyDownListener = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onHide() + } + }, + [onHide] + ) + + useEventListener('keydown', keyDownListener) + + useEffect(() => { + if (mathRef.current) { + mathRef.current.replaceChildren(mathContent) + } + }, [mathContent]) + + return ( + <> +
+ + + + + + + +
  • + + Esc + + } + > + {t('hide')} + +
  • +
  • + + {t('disable')} + +
  • +
    + + } + bs3={ + + + + + + +
    +
    + {t('hide')} +
    +
    + {t('temporarily_hides_the_preview')} +
    +
    +
    + Esc +
    +
    + +
    +
    + {t('disable')} +
    +
    + {t('permanently_disables_the_preview')} +
    +
    +
    +
    +
    + } + /> +
    + + {showDisableModal && ( + + + {t('disable_equation_preview')} + + + + {t('disable_equation_preview_confirm')} +
    + }} + /> +
    + + + + {t('cancel')} + + setMathPreview(false)}> + {t('disable')} + + +
    + )} + + ) +} + +export default MathPreviewTooltipContainer diff --git a/services/web/frontend/js/features/source-editor/extensions/math-preview.tsx b/services/web/frontend/js/features/source-editor/extensions/math-preview.ts similarity index 67% rename from services/web/frontend/js/features/source-editor/extensions/math-preview.tsx rename to services/web/frontend/js/features/source-editor/extensions/math-preview.ts index 9a520444c0..a28a1c9508 100644 --- a/services/web/frontend/js/features/source-editor/extensions/math-preview.tsx +++ b/services/web/frontend/js/features/source-editor/extensions/math-preview.ts @@ -9,6 +9,7 @@ import { Compartment, EditorState, Extension, + StateEffect, StateField, TransactionSpec, } from '@codemirror/state' @@ -22,13 +23,11 @@ import { import { documentCommands } from '../languages/latex/document-commands' import { debugConsole } from '@/utils/debugging' import { isSplitTestEnabled } from '@/utils/splitTestUtils' -import ReactDOM from 'react-dom' -import { SplitTestProvider } from '@/shared/context/split-test-context' -import SplitTestBadge from '@/shared/components/split-test-badge' import { nodeHasError } from '../utils/tree-operations/common' import { documentEnvironments } from '../languages/latex/document-environments' const REPOSITION_EVENT = 'editor:repositionMathTooltips' +const HIDE_TOOLTIP_EVENT = 'editor:hideMathTooltip' export const mathPreview = (enabled: boolean): Extension => { if (!isSplitTestEnabled('math-preview')) { @@ -40,38 +39,91 @@ export const mathPreview = (enabled: boolean): Extension => { ) } +export const hideTooltipEffect = StateEffect.define() + const mathPreviewConf = new Compartment() export const setMathPreview = (enabled: boolean): TransactionSpec => ({ effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []), }) -const mathPreviewStateField = StateField.define({ - create: buildTooltip, +export const mathPreviewStateField = StateField.define<{ + tooltip: Tooltip | null + mathContent: HTMLDivElement | null + hide: boolean +}>({ + create: buildInitialState, - update(tooltips, tr) { - if (tr.docChanged || tr.selection) { - tooltips = buildTooltip(tr.state) + update(state, tr) { + for (const effect of tr.effects) { + if (effect.is(hideTooltipEffect)) { + return { tooltip: null, hide: true, mathContent: null } + } } - return tooltips + if (tr.docChanged || tr.selection) { + const mathContainer = getMathContainer(tr.state) + + if (mathContainer) { + if (state.hide) { + return { tooltip: null, hide: true, mathContent: null } + } else { + const mathContent = buildTooltipContent(tr.state, mathContainer) + + return { + tooltip: buildTooltip(mathContainer, mathContent), + mathContent, + hide: false, + } + } + } + + return { tooltip: null, hide: false, mathContent: null } + } + + return state }, provide: field => [ - showTooltip.compute([field], state => state.field(field)), + showTooltip.compute([field], state => state.field(field).tooltip), ViewPlugin.define(view => { const listener = () => repositionTooltips(view) + const hideTooltip = () => { + view.dispatch({ + effects: hideTooltipEffect.of(null), + }) + } + window.addEventListener(REPOSITION_EVENT, listener) + window.addEventListener(HIDE_TOOLTIP_EVENT, hideTooltip) + return { destroy() { window.removeEventListener(REPOSITION_EVENT, listener) + window.removeEventListener(HIDE_TOOLTIP_EVENT, hideTooltip) }, } }), ], }) +function buildInitialState(state: EditorState) { + const mathContainer = getMathContainer(state) + + if (mathContainer) { + const mathContent = buildTooltipContent(state, mathContainer) + + return { + tooltip: buildTooltip(mathContainer, mathContent), + mathContent, + hide: false, + } + } + + return { tooltip: null, hide: false, mathContent: null } +} + const renderMath = async ( content: string, displayMode: boolean, @@ -96,17 +148,11 @@ const renderMath = async ( element.append(math) } -function buildTooltip(state: EditorState): Tooltip | null { - const range = state.selection.main - - if (!range.empty) { - return null - } - - const mathContainer = getMathContainer(state, range.from) - const content = buildTooltipContent(state, mathContainer) - - if (!content || !mathContainer) { +function buildTooltip( + mathContainer: MathContainer, + mathContent: HTMLDivElement | null +): Tooltip | null { + if (!mathContent || !mathContainer) { return null } @@ -117,19 +163,22 @@ function buildTooltip(state: EditorState): Tooltip | null { arrow: false, create() { const dom = document.createElement('div') - dom.append(content) - const badge = renderSplitTestBadge() - dom.append(badge) - dom.className = 'ol-cm-math-tooltip' + dom.classList.add('ol-cm-math-tooltip-container') return { dom, overlap: true, offset: { x: 0, y: 8 } } }, } } -const getMathContainer = (state: EditorState, pos: number) => { +const getMathContainer = (state: EditorState) => { + const range = state.selection.main + + if (!range.empty) { + return null + } + // if anywhere inside Math, find the whole Math node - const ancestorNode = mathAncestorNode(state, pos) + const ancestorNode = mathAncestorNode(state, range.from) if (!ancestorNode) return null const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math') @@ -182,30 +231,16 @@ const buildTooltipContent = ( return element } -const renderSplitTestBadge = () => { - const element = document.createElement('span') - ReactDOM.render( - - - , - element - ) - return element -} - /** * Styles for the preview tooltip */ const mathPreviewTheme = EditorView.baseTheme({ - '&light .ol-cm-math-tooltip': { + '&light .ol-cm-math-tooltip-container': { boxShadow: '0px 2px 4px 0px #1e253029', border: '1px solid #e7e9ee !important', backgroundColor: 'white !important', }, - '&dark .ol-cm-math-tooltip': { + '&dark .ol-cm-math-tooltip-container': { boxShadow: '0px 2px 4px 0px #1e253029', border: '1px solid #2f3a4c !important', backgroundColor: '#1b222c !important', diff --git a/services/web/frontend/stylesheets/app/editor/math-preview.less b/services/web/frontend/stylesheets/app/editor/math-preview.less index 47a70275d3..5b158499c4 100644 --- a/services/web/frontend/stylesheets/app/editor/math-preview.less +++ b/services/web/frontend/stylesheets/app/editor/math-preview.less @@ -1,9 +1,74 @@ -.ol-cm-math-tooltip { +.ol-cm-math-tooltip-container { + position: relative; border-radius: 4px; max-height: 400px; max-width: 800px; - overflow: auto; - padding: 8px; + overflow: visible; +} + +.ol-cm-math-tooltip { display: flex; gap: 8px; + overflow: auto; + padding: 8px; + + .dropdown { + position: static; + } +} + +.math-tooltip-options-toggle { + border: none; + padding: 0; + width: 20px; + height: 20px; + background-color: transparent; + color: black !important; + + &:focus { + background-color: transparent; + } + + &:hover, + &:active { + background-color: @neutral-20; + } +} + +.math-preview-tooltip-menu { + top: 28px; + right: 8px; +} + +.dropdown-menu { + .math-preview-tooltip-option { + a { + display: flex; + gap: 16px; + align-items: center; + } + + div { + padding: 0; + } + } +} + +.math-preview-tooltip-option-content { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.math-preview-tooltip-option-label { + color: @content-primary; +} + +.math-preview-tooltip-option-description { + color: @content-secondary; + font-size: 12px; +} + +.math-preview-tooltip-option-shortcut { + color: @content-secondary; } diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss index 1e97a1f67e..0d33dafcba 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/all.scss @@ -17,7 +17,6 @@ @import 'editor/review-panel'; @import 'editor/chat'; @import 'editor/history'; -@import 'editor/math-preview'; @import 'subscription'; @import 'editor/pdf'; @import 'editor/compile-button'; @@ -26,6 +25,7 @@ @import 'editor/tags-input'; @import 'editor/review-panel-new'; @import 'editor/table-generator-column-width-modal'; +@import 'editor/math-preview'; @import 'website-redesign'; @import 'group-settings'; @import 'templates-v2'; diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/math-preview.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/math-preview.scss index 5315ecfcfb..04c38c8e2a 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/math-preview.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/math-preview.scss @@ -1,9 +1,34 @@ -.ol-cm-math-tooltip { +.ol-cm-math-tooltip-container { + position: relative; border-radius: var(--border-radius-base); max-height: 400px; max-width: 800px; - overflow: auto; - padding: var(--spacing-04); + overflow: visible; +} + +.ol-cm-math-tooltip { display: flex; gap: var(--spacing-04); + overflow: auto; + padding: var(--spacing-04); + + .dropdown { + position: static; + } +} + +.math-tooltip-options-toggle { + border: none; + padding: 0; + width: 20px; + height: 20px; + + &::after { + content: none; + } +} + +.math-tooltip-options-keyboard-shortcut { + color: $content-secondary; + font-size: var(--font-size-02); } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 4133731b72..ba2734c919 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -468,6 +468,10 @@ "details_provided_by_google_explanation": "Your details were provided by your Google account. Please check you’re happy with them.", "dictionary": "Dictionary", "did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features to everyone at __institutionName__?", + "disable": "Disable", + "disable_equation_preview": "Disable equation preview", + "disable_equation_preview_confirm": "This will disable equation preview for you in all projects.", + "disable_equation_preview_enable": "You can enable it again from the Menu.", "disable_single_sign_on": "Disable single sign-on", "disable_sso": "Disable SSO", "disable_stop_on_first_error": "Disable “Stop on first error”", @@ -884,6 +888,7 @@ "help_articles_matching": "Help articles matching your subject", "help_improve_overleaf_fill_out_this_survey": "If you would like to help us improve Overleaf, please take a moment to fill out <0>this survey.", "help_improve_screen_reader_fill_out_this_survey": "Help us improve your experience using a screen reader with __appName__ by filling out this quick survey.", + "hide": "Hide", "hide_configuration": "Hide configuration", "hide_deleted_user": "Hide deleted users", "hide_document_preamble": "Hide document preamble", @@ -1510,6 +1515,7 @@ "per_year": "per year", "percent_discount_for_groups": "__appName__ offers a __percent__% educational discount for groups of __size__ or more.", "percent_is_the_percentage_of_the_line_width": "% is the percentage of the line width", + "permanently_disables_the_preview": "Permanently disables the preview", "personal": "Personal", "personal_library": "Personal library", "personalized_onboarding": "Personalized onboarding", @@ -2102,6 +2108,7 @@ "templates_lowercase": "templates", "templates_page_summary": "Start your projects with quality LaTeX templates for journals, CVs, resumes, papers, presentations, assignments, letters, project reports, and more. Search or browse below.", "templates_page_title": "Templates - Journals, CVs, Presentations, Reports and More", + "temporarily_hides_the_preview": "Temporarily hides the preview", "ten_collaborators_per_project": "10 collaborators per project", "ten_per_project": "10 per project", "terminated": "Compilation cancelled",