Merge pull request #21526 from overleaf/dp-dismiss-equation-preview

Add options to disable/hide equation preview

GitOrigin-RevId: 5f71b4c747bf27ae816bdfe32c6e3e5a42f99508
This commit is contained in:
David 2024-11-06 15:53:32 +00:00 committed by Copybot
parent d95981cb0c
commit dfcc805549
8 changed files with 412 additions and 50 deletions

View file

@ -342,6 +342,10 @@
"details_provided_by_google_explanation": "", "details_provided_by_google_explanation": "",
"dictionary": "", "dictionary": "",
"did_you_know_institution_providing_professional": "", "did_you_know_institution_providing_professional": "",
"disable": "",
"disable_equation_preview": "",
"disable_equation_preview_confirm": "",
"disable_equation_preview_enable": "",
"disable_single_sign_on": "", "disable_single_sign_on": "",
"disable_sso": "", "disable_sso": "",
"disable_stop_on_first_error": "", "disable_stop_on_first_error": "",
@ -612,6 +616,7 @@
"help_articles_matching": "", "help_articles_matching": "",
"help_improve_overleaf_fill_out_this_survey": "", "help_improve_overleaf_fill_out_this_survey": "",
"help_improve_screen_reader_fill_out_this_survey": "", "help_improve_screen_reader_fill_out_this_survey": "",
"hide": "",
"hide_configuration": "", "hide_configuration": "",
"hide_deleted_user": "", "hide_deleted_user": "",
"hide_document_preamble": "", "hide_document_preamble": "",
@ -1047,6 +1052,7 @@
"pending_invite": "", "pending_invite": "",
"percent_discount_for_groups": "", "percent_discount_for_groups": "",
"percent_is_the_percentage_of_the_line_width": "", "percent_is_the_percentage_of_the_line_width": "",
"permanently_disables_the_preview": "",
"personal_library": "", "personal_library": "",
"plan": "", "plan": "",
"plan_tooltip": "", "plan_tooltip": "",
@ -1508,6 +1514,7 @@
"template_description": "", "template_description": "",
"template_title_taken_from_project_title": "", "template_title_taken_from_project_title": "",
"templates": "", "templates": "",
"temporarily_hides_the_preview": "",
"terminated": "", "terminated": "",
"test": "", "test": "",
"test_configuration": "", "test_configuration": "",

View file

@ -18,6 +18,7 @@ import {
CodeMirrorStateContext, CodeMirrorStateContext,
CodeMirrorViewContext, CodeMirrorViewContext,
} from './codemirror-context' } from './codemirror-context'
import MathPreviewTooltip from './math-preview-tooltip'
// TODO: remove this when definitely no longer used // TODO: remove this when definitely no longer used
export * from './codemirror-context' export * from './codemirror-context'
@ -39,6 +40,7 @@ function CodeMirrorEditor() {
const isMounted = useIsMounted() const isMounted = useIsMounted()
const newReviewPanel = useFeatureFlag('review-panel-redesign') const newReviewPanel = useFeatureFlag('review-panel-redesign')
const enableMathPreview = useFeatureFlag('math-preview')
// create the view using the initial state and intercept transactions // create the view using the initial state and intercept transactions
const viewRef = useRef<EditorView | null>(null) const viewRef = useRef<EditorView | null>(null)
@ -78,6 +80,7 @@ function CodeMirrorEditor() {
)} )}
<CodeMirrorCommandTooltip /> <CodeMirrorCommandTooltip />
{enableMathPreview && <MathPreviewTooltip />}
{newReviewPanel && <ReviewTooltipMenu />} {newReviewPanel && <ReviewTooltipMenu />}
<ReviewPanelMigration /> <ReviewPanelMigration />

View file

@ -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(
<MathPreviewTooltip mathContent={mathContent} />,
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<HTMLSpanElement>(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 (
<>
<div className="ol-cm-math-tooltip">
<span ref={mathRef} />
<SplitTestBadge
displayOnVariants={['enabled']}
splitTestName="math-preview"
/>
<BootstrapVersionSwitcher
bs5={
<Dropdown align="end">
<DropdownToggle
id="some-id"
className="math-tooltip-options-toggle"
variant="secondary"
size="sm"
>
<MaterialIcon
type="more_vert"
accessibilityLabel={t('more_options')}
/>
</DropdownToggle>
<DropdownMenu flip={false}>
<li>
<OLDropdownMenuItem
onClick={onHide}
description={t('temporarily_hides_the_preview')}
trailingIcon={
<span className="math-tooltip-options-keyboard-shortcut">
Esc
</span>
}
>
{t('hide')}
</OLDropdownMenuItem>
</li>
<li>
<OLDropdownMenuItem
onClick={openDisableModal}
description={t('permanently_disables_the_preview')}
>
{t('disable')}
</OLDropdownMenuItem>
</li>
</DropdownMenu>
</Dropdown>
}
bs3={
<ControlledDropdown id="math-preview-tooltip-options" pullRight>
<BS3Dropdown.Toggle
noCaret
bsSize="small"
bsStyle={null}
className="math-tooltip-options-toggle"
>
<MaterialIcon
type="more_vert"
accessibilityLabel={t('more_options')}
/>
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="math-preview-tooltip-menu">
<BS3MenuItem
className="math-preview-tooltip-option"
onClick={onHide}
>
<div className="math-preview-tooltip-option-content">
<div className="math-preview-tooltip-option-label">
{t('hide')}
</div>
<div className="math-preview-tooltip-option-description">
{t('temporarily_hides_the_preview')}
</div>
</div>
<div className="math-preview-tooltip-option-shortcut">
Esc
</div>
</BS3MenuItem>
<BS3MenuItem
className="math-preview-tooltip-option"
onClick={openDisableModal}
>
<div className="math-preview-tooltip-option-content">
<div className="math-preview-tooltip-option-label">
{t('disable')}
</div>
<div className="math-preview-tooltip-option-description">
{t('permanently_disables_the_preview')}
</div>
</div>
</BS3MenuItem>
</BS3Dropdown.Menu>
</ControlledDropdown>
}
/>
</div>
{showDisableModal && (
<OLModal show onHide={closeDisableModal}>
<OLModalHeader>
<OLModalTitle>{t('disable_equation_preview')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
{t('disable_equation_preview_confirm')}
<br />
<Trans
i18nKey="disable_equation_preview_enable"
components={{ b: <strong /> }}
/>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={closeDisableModal}>
{t('cancel')}
</OLButton>
<OLButton variant="danger" onClick={() => setMathPreview(false)}>
{t('disable')}
</OLButton>
</OLModalFooter>
</OLModal>
)}
</>
)
}
export default MathPreviewTooltipContainer

View file

@ -9,6 +9,7 @@ import {
Compartment, Compartment,
EditorState, EditorState,
Extension, Extension,
StateEffect,
StateField, StateField,
TransactionSpec, TransactionSpec,
} from '@codemirror/state' } from '@codemirror/state'
@ -22,13 +23,11 @@ import {
import { documentCommands } from '../languages/latex/document-commands' import { documentCommands } from '../languages/latex/document-commands'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { isSplitTestEnabled } from '@/utils/splitTestUtils' 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 { nodeHasError } from '../utils/tree-operations/common'
import { documentEnvironments } from '../languages/latex/document-environments' import { documentEnvironments } from '../languages/latex/document-environments'
const REPOSITION_EVENT = 'editor:repositionMathTooltips' const REPOSITION_EVENT = 'editor:repositionMathTooltips'
const HIDE_TOOLTIP_EVENT = 'editor:hideMathTooltip'
export const mathPreview = (enabled: boolean): Extension => { export const mathPreview = (enabled: boolean): Extension => {
if (!isSplitTestEnabled('math-preview')) { if (!isSplitTestEnabled('math-preview')) {
@ -40,38 +39,91 @@ export const mathPreview = (enabled: boolean): Extension => {
) )
} }
export const hideTooltipEffect = StateEffect.define<null>()
const mathPreviewConf = new Compartment() const mathPreviewConf = new Compartment()
export const setMathPreview = (enabled: boolean): TransactionSpec => ({ export const setMathPreview = (enabled: boolean): TransactionSpec => ({
effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []), effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []),
}) })
const mathPreviewStateField = StateField.define<Tooltip | null>({ export const mathPreviewStateField = StateField.define<{
create: buildTooltip, tooltip: Tooltip | null
mathContent: HTMLDivElement | null
hide: boolean
}>({
create: buildInitialState,
update(tooltips, tr) { update(state, tr) {
if (tr.docChanged || tr.selection) { for (const effect of tr.effects) {
tooltips = buildTooltip(tr.state) 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 => [ provide: field => [
showTooltip.compute([field], state => state.field(field)), showTooltip.compute([field], state => state.field(field).tooltip),
ViewPlugin.define(view => { ViewPlugin.define(view => {
const listener = () => repositionTooltips(view) const listener = () => repositionTooltips(view)
const hideTooltip = () => {
view.dispatch({
effects: hideTooltipEffect.of(null),
})
}
window.addEventListener(REPOSITION_EVENT, listener) window.addEventListener(REPOSITION_EVENT, listener)
window.addEventListener(HIDE_TOOLTIP_EVENT, hideTooltip)
return { return {
destroy() { destroy() {
window.removeEventListener(REPOSITION_EVENT, listener) 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 ( const renderMath = async (
content: string, content: string,
displayMode: boolean, displayMode: boolean,
@ -96,17 +148,11 @@ const renderMath = async (
element.append(math) element.append(math)
} }
function buildTooltip(state: EditorState): Tooltip | null { function buildTooltip(
const range = state.selection.main mathContainer: MathContainer,
mathContent: HTMLDivElement | null
if (!range.empty) { ): Tooltip | null {
return null if (!mathContent || !mathContainer) {
}
const mathContainer = getMathContainer(state, range.from)
const content = buildTooltipContent(state, mathContainer)
if (!content || !mathContainer) {
return null return null
} }
@ -117,19 +163,22 @@ function buildTooltip(state: EditorState): Tooltip | null {
arrow: false, arrow: false,
create() { create() {
const dom = document.createElement('div') const dom = document.createElement('div')
dom.append(content) dom.classList.add('ol-cm-math-tooltip-container')
const badge = renderSplitTestBadge()
dom.append(badge)
dom.className = 'ol-cm-math-tooltip'
return { dom, overlap: true, offset: { x: 0, y: 8 } } 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 // if anywhere inside Math, find the whole Math node
const ancestorNode = mathAncestorNode(state, pos) const ancestorNode = mathAncestorNode(state, range.from)
if (!ancestorNode) return null if (!ancestorNode) return null
const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math') const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math')
@ -182,30 +231,16 @@ const buildTooltipContent = (
return element return element
} }
const renderSplitTestBadge = () => {
const element = document.createElement('span')
ReactDOM.render(
<SplitTestProvider>
<SplitTestBadge
displayOnVariants={['enabled']}
splitTestName="math-preview"
/>
</SplitTestProvider>,
element
)
return element
}
/** /**
* Styles for the preview tooltip * Styles for the preview tooltip
*/ */
const mathPreviewTheme = EditorView.baseTheme({ const mathPreviewTheme = EditorView.baseTheme({
'&light .ol-cm-math-tooltip': { '&light .ol-cm-math-tooltip-container': {
boxShadow: '0px 2px 4px 0px #1e253029', boxShadow: '0px 2px 4px 0px #1e253029',
border: '1px solid #e7e9ee !important', border: '1px solid #e7e9ee !important',
backgroundColor: 'white !important', backgroundColor: 'white !important',
}, },
'&dark .ol-cm-math-tooltip': { '&dark .ol-cm-math-tooltip-container': {
boxShadow: '0px 2px 4px 0px #1e253029', boxShadow: '0px 2px 4px 0px #1e253029',
border: '1px solid #2f3a4c !important', border: '1px solid #2f3a4c !important',
backgroundColor: '#1b222c !important', backgroundColor: '#1b222c !important',

View file

@ -1,9 +1,74 @@
.ol-cm-math-tooltip { .ol-cm-math-tooltip-container {
position: relative;
border-radius: 4px; border-radius: 4px;
max-height: 400px; max-height: 400px;
max-width: 800px; max-width: 800px;
overflow: auto; overflow: visible;
padding: 8px; }
.ol-cm-math-tooltip {
display: flex; display: flex;
gap: 8px; 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;
} }

View file

@ -17,7 +17,6 @@
@import 'editor/review-panel'; @import 'editor/review-panel';
@import 'editor/chat'; @import 'editor/chat';
@import 'editor/history'; @import 'editor/history';
@import 'editor/math-preview';
@import 'subscription'; @import 'subscription';
@import 'editor/pdf'; @import 'editor/pdf';
@import 'editor/compile-button'; @import 'editor/compile-button';
@ -26,6 +25,7 @@
@import 'editor/tags-input'; @import 'editor/tags-input';
@import 'editor/review-panel-new'; @import 'editor/review-panel-new';
@import 'editor/table-generator-column-width-modal'; @import 'editor/table-generator-column-width-modal';
@import 'editor/math-preview';
@import 'website-redesign'; @import 'website-redesign';
@import 'group-settings'; @import 'group-settings';
@import 'templates-v2'; @import 'templates-v2';

View file

@ -1,9 +1,34 @@
.ol-cm-math-tooltip { .ol-cm-math-tooltip-container {
position: relative;
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
max-height: 400px; max-height: 400px;
max-width: 800px; max-width: 800px;
overflow: auto; overflow: visible;
padding: var(--spacing-04); }
.ol-cm-math-tooltip {
display: flex; display: flex;
gap: var(--spacing-04); 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);
} }

View file

@ -468,6 +468,10 @@
"details_provided_by_google_explanation": "Your details were provided by your Google account. Please check youre happy with them.", "details_provided_by_google_explanation": "Your details were provided by your Google account. Please check youre happy with them.",
"dictionary": "Dictionary", "dictionary": "Dictionary",
"did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features</0> to everyone at __institutionName__?", "did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features</0> 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 <b>Menu</b>.",
"disable_single_sign_on": "Disable single sign-on", "disable_single_sign_on": "Disable single sign-on",
"disable_sso": "Disable SSO", "disable_sso": "Disable SSO",
"disable_stop_on_first_error": "Disable “Stop on first error”", "disable_stop_on_first_error": "Disable “Stop on first error”",
@ -884,6 +888,7 @@
"help_articles_matching": "Help articles matching your subject", "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</0>.", "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</0>.",
"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.", "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_configuration": "Hide configuration",
"hide_deleted_user": "Hide deleted users", "hide_deleted_user": "Hide deleted users",
"hide_document_preamble": "Hide document preamble", "hide_document_preamble": "Hide document preamble",
@ -1510,6 +1515,7 @@
"per_year": "per year", "per_year": "per year",
"percent_discount_for_groups": "__appName__ offers a __percent__% educational discount for groups of __size__ or more.", "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", "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": "Personal",
"personal_library": "Personal library", "personal_library": "Personal library",
"personalized_onboarding": "Personalized onboarding", "personalized_onboarding": "Personalized onboarding",
@ -2102,6 +2108,7 @@
"templates_lowercase": "templates", "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_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", "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_collaborators_per_project": "10 collaborators per project",
"ten_per_project": "10 per project", "ten_per_project": "10 per project",
"terminated": "Compilation cancelled", "terminated": "Compilation cancelled",