瀏覽代碼

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

Add options to disable/hide equation preview

GitOrigin-RevId: 5f71b4c747bf27ae816bdfe32c6e3e5a42f99508
David 1 年之前
父節點
當前提交
dfcc805549

+ 7 - 0
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": "",

+ 3 - 0
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<EditorView | null>(null)
@@ -78,6 +80,7 @@ function CodeMirrorEditor() {
           )}
           <CodeMirrorCommandTooltip />
 
+          {enableMathPreview && <MathPreviewTooltip />}
           {newReviewPanel && <ReviewTooltipMenu />}
           <ReviewPanelMigration />
 

+ 220 - 0
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(
+    <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

+ 77 - 42
services/web/frontend/js/features/source-editor/extensions/math-preview.tsx → 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<null>()
+
 const mathPreviewConf = new Compartment()
 
 export const setMathPreview = (enabled: boolean): TransactionSpec => ({
   effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []),
 })
 
-const mathPreviewStateField = StateField.define<Tooltip | null>({
-  create: buildTooltip,
+export const mathPreviewStateField = StateField.define<{
+  tooltip: Tooltip | null
+  mathContent: HTMLDivElement | null
+  hide: boolean
+}>({
+  create: buildInitialState,
+
+  update(state, tr) {
+    for (const effect of tr.effects) {
+      if (effect.is(hideTooltipEffect)) {
+        return { tooltip: null, hide: true, mathContent: null }
+      }
+    }
 
-  update(tooltips, tr) {
     if (tr.docChanged || tr.selection) {
-      tooltips = buildTooltip(tr.state)
+      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 tooltips
+    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(
-    <SplitTestProvider>
-      <SplitTestBadge
-        displayOnVariants={['enabled']}
-        splitTestName="math-preview"
-      />
-    </SplitTestProvider>,
-    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',

+ 67 - 2
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: 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;
-  gap: 8px;
+  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;
 }

+ 1 - 1
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';

+ 28 - 3
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);
 }

+ 7 - 0
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</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_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</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.",
+  "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",