Procházet zdrojové kódy

[cm6] Improve performance of the editor toolbar (#13915)

* Memoize figure and math dropdowns
* Only build section heading overlay when open
* Memoise currentLevel
* Remove memo from ToolbarOverflow
* Calculate listDepth in the container component
* Avoid using document.querySelector

GitOrigin-RevId: d5ec8817d35d04e0e2c60c8eecc8678ede69f82a
Alf Eaton před 3 roky
rodič
revize
ff7eec48de

+ 10 - 1
services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx

@@ -21,6 +21,7 @@ import getMeta from '../../../utils/meta'
 import { isVisual } from '../extensions/visual/visual'
 import SplitTestBadge from '../../../shared/components/split-test-badge'
 import { language } from '@codemirror/language'
+import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
 
 export const CodeMirrorToolbar = () => {
   const view = useCodeMirrorViewContext()
@@ -48,6 +49,8 @@ const Toolbar = memo(function Toolbar() {
   const languageName = state.facet(language)?.name
   const visual = isVisual(view)
 
+  const listDepth = minimumListDepthForSelection(state)
+
   const {
     open: overflowOpen,
     onToggle: setOverflowOpen,
@@ -108,7 +111,12 @@ const Toolbar = memo(function Toolbar() {
   return (
     <div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
       {showSourceToolbar && <EditorSwitch />}
-      <ToolbarItems state={state} languageName={languageName} visual={visual} />
+      <ToolbarItems
+        state={state}
+        languageName={languageName}
+        visual={visual}
+        listDepth={listDepth}
+      />
       <div
         className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch"
         ref={overflowBeforeRef}
@@ -125,6 +133,7 @@ const Toolbar = memo(function Toolbar() {
             overflowed={overflowedItemsRef.current}
             languageName={languageName}
             visual={visual}
+            listDepth={listDepth}
           />
         </ToolbarOverflow>
         <div className="formatting-buttons-wrapper" />

+ 1 - 1
services/web/frontend/js/features/source-editor/components/toolbar/button-menu.tsx

@@ -57,7 +57,7 @@ export const ToolbarButtonMenu: FC<{
       show={open}
       target={target.current}
       placement="bottom"
-      container={document.querySelector('.cm-editor')}
+      container={view.dom}
       containerPadding={0}
       animation
       onHide={() => onToggle(false)}

+ 3 - 3
services/web/frontend/js/features/source-editor/components/toolbar/insert-figure-dropdown.tsx

@@ -1,14 +1,14 @@
 import { ListGroupItem } from 'react-bootstrap'
 import { ToolbarButtonMenu } from './button-menu'
 import Icon from '../../../../shared/components/icon'
-import { useCallback } from 'react'
+import { memo, useCallback } from 'react'
 import { FigureModalSource } from '../figure-modal/figure-modal-context'
 import { useTranslation } from 'react-i18next'
 import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
 import { useCodeMirrorViewContext } from '../codemirror-editor'
 import { insertFigure } from '../../extensions/toolbar/commands'
 
-export const InsertFigureDropdown = () => {
+export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
   const { t } = useTranslation()
   const view = useCodeMirrorViewContext()
   const openFigureModal = useCallback(
@@ -57,4 +57,4 @@ export const InsertFigureDropdown = () => {
       </ListGroupItem>
     </ToolbarButtonMenu>
   )
-}
+})

+ 3 - 2
services/web/frontend/js/features/source-editor/components/toolbar/math-dropdown.tsx

@@ -8,8 +8,9 @@ import {
   wrapInDisplayMath,
   wrapInInlineMath,
 } from '../../extensions/toolbar/commands'
+import { memo } from 'react'
 
-export function MathDropdown() {
+export const MathDropdown = memo(function MathDropdown() {
   const { t } = useTranslation()
   const view = useCodeMirrorViewContext()
 
@@ -46,4 +47,4 @@ export function MathDropdown() {
       </ListGroupItem>
     </ToolbarButtonMenu>
   )
-}
+})

+ 8 - 5
services/web/frontend/js/features/source-editor/components/toolbar/overflow.tsx

@@ -1,7 +1,8 @@
-import { FC, LegacyRef, memo } from 'react'
+import { FC, LegacyRef } from 'react'
 import { Button, Overlay, Popover } from 'react-bootstrap'
 import classnames from 'classnames'
 import Icon from '../../../../shared/components/icon'
+import { useCodeMirrorViewContext } from '../codemirror-editor'
 
 export const ToolbarOverflow: FC<{
   overflowed: boolean
@@ -9,14 +10,16 @@ export const ToolbarOverflow: FC<{
   overflowOpen: boolean
   setOverflowOpen: (open: boolean) => void
   overflowRef?: LegacyRef<Popover>
-}> = memo(function ToolbarOverflow({
+}> = ({
   overflowed,
   target,
   overflowOpen,
   setOverflowOpen,
   overflowRef,
   children,
-}) {
+}) => {
+  const view = useCodeMirrorViewContext()
+
   const className = classnames(
     'ol-cm-toolbar-button',
     'ol-cm-toolbar-overflow-toggle',
@@ -48,7 +51,7 @@ export const ToolbarOverflow: FC<{
         show={overflowOpen}
         target={target}
         placement="bottom"
-        container={document.querySelector('.cm-editor')}
+        container={view.dom}
         containerPadding={0}
         animation
         onHide={() => setOverflowOpen(false)}
@@ -59,4 +62,4 @@ export const ToolbarOverflow: FC<{
       </Overlay>
     </>
   )
-})
+}

+ 52 - 46
services/web/frontend/js/features/source-editor/components/toolbar/section-heading-dropdown.tsx

@@ -7,7 +7,7 @@ import {
   findCurrentSectionHeadingLevel,
   setSectionHeadingLevel,
 } from '../../extensions/toolbar/sections'
-import { useCallback, useRef } from 'react'
+import { useCallback, useMemo, useRef } from 'react'
 import { Overlay, Popover } from 'react-bootstrap'
 import useEventListener from '../../../../shared/hooks/use-event-listener'
 import useDropdown from '../../../../shared/hooks/use-dropdown'
@@ -42,7 +42,11 @@ export const SectionHeadingDropdown = () => {
 
   const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
 
-  const currentLevel = findCurrentSectionHeadingLevel(state)
+  const currentLevel = useMemo(
+    () => findCurrentSectionHeadingLevel(state),
+    [state]
+  )
+
   const currentLabel = currentLevel
     ? levels.get(currentLevel.level) ?? currentLevel.level
     : '---'
@@ -64,52 +68,54 @@ export const SectionHeadingDropdown = () => {
         <Icon type="caret-down" fw />
       </button>
 
-      <Overlay
-        show={overflowOpen}
-        onHide={() => setOverflowOpen(false)}
-        animation={false}
-        container={document.querySelector('.cm-editor')}
-        containerPadding={0}
-        placement="bottom"
-        rootClose
-        target={toggleButtonRef.current ?? undefined}
-      >
-        <Popover
-          id="popover-toolbar-section-heading"
-          className="ol-cm-toolbar-menu-popover"
+      {overflowOpen && (
+        <Overlay
+          show
+          onHide={() => setOverflowOpen(false)}
+          animation={false}
+          container={view.dom}
+          containerPadding={0}
+          placement="bottom"
+          rootClose
+          target={toggleButtonRef.current ?? undefined}
         >
-          <div
-            className="ol-cm-toolbar-menu"
-            id="section-heading-menu"
-            role="menu"
-            aria-labelledby="section-heading-menu-button"
+          <Popover
+            id="popover-toolbar-section-heading"
+            className="ol-cm-toolbar-menu-popover"
           >
-            {levelsEntries.map(([level, label]) => (
-              <button
-                type="button"
-                role="menuitem"
-                key={level}
-                onClick={() => {
-                  emitToolbarEvent(view, 'section-level-change')
-                  setSectionHeadingLevel(view, level)
-                  view.focus()
-                  setOverflowOpen(false)
-                }}
-                className={classnames(
-                  'ol-cm-toolbar-menu-item',
-                  `section-level-${level}`,
-                  {
-                    'ol-cm-toolbar-menu-item-active':
-                      level === currentLevel?.level,
-                  }
-                )}
-              >
-                {label}
-              </button>
-            ))}
-          </div>
-        </Popover>
-      </Overlay>
+            <div
+              className="ol-cm-toolbar-menu"
+              id="section-heading-menu"
+              role="menu"
+              aria-labelledby="section-heading-menu-button"
+            >
+              {levelsEntries.map(([level, label]) => (
+                <button
+                  type="button"
+                  role="menuitem"
+                  key={level}
+                  onClick={() => {
+                    emitToolbarEvent(view, 'section-level-change')
+                    setSectionHeadingLevel(view, level)
+                    view.focus()
+                    setOverflowOpen(false)
+                  }}
+                  className={classnames(
+                    'ol-cm-toolbar-menu-item',
+                    `section-level-${level}`,
+                    {
+                      'ol-cm-toolbar-menu-item-active':
+                        level === currentLevel?.level,
+                    }
+                  )}
+                >
+                  {label}
+                </button>
+              ))}
+            </div>
+          </Popover>
+        </Overlay>
+      )}
     </>
   )
 }

+ 9 - 6
services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx

@@ -4,10 +4,7 @@ import { EditorView } from '@codemirror/view'
 import { useEditorContext } from '../../../../shared/context/editor-context'
 import useScopeEventEmitter from '../../../../shared/hooks/use-scope-event-emitter'
 import { useLayoutContext } from '../../../../shared/context/layout-context'
-import {
-  minimumListDepthForSelection,
-  withinFormattingCommand,
-} from '../../utils/tree-operations/ancestors'
+import { withinFormattingCommand } from '../../utils/tree-operations/ancestors'
 import { ToolbarButton } from './toolbar-button'
 import { redo, undo } from '@codemirror/commands'
 import * as commands from '../../extensions/toolbar/commands'
@@ -25,11 +22,17 @@ export const ToolbarItems: FC<{
   overflowed?: Set<string>
   languageName?: string
   visual: boolean
-}> = memo(function ToolbarItems({ state, overflowed, languageName, visual }) {
+  listDepth: number
+}> = memo(function ToolbarItems({
+  state,
+  overflowed,
+  languageName,
+  visual,
+  listDepth,
+}) {
   const { t } = useTranslation()
   const { toggleSymbolPalette, showSymbolPalette } = useEditorContext()
   const isActive = withinFormattingCommand(state)
-  const listDepth = minimumListDepthForSelection(state)
   const addCommentEmitter = useScopeEventEmitter('comment:start_adding')
   const { setReviewPanelOpen } = useLayoutContext()
   const splitTestVariants = getMeta('ol-splitTestVariants', {})