Merge pull request #10397 from overleaf/td-memoize-file-outline

Memoize file outline

GitOrigin-RevId: cb086bab2b6ead251362180d776e7eaff18fc639
This commit is contained in:
Mathias Jakobsen 2022-11-14 09:42:06 +00:00 committed by Copybot
parent 6ae22ff596
commit 81e2265e72
4 changed files with 57 additions and 21 deletions

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef, memo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed' import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
import classNames from 'classnames' import classNames from 'classnames'
@ -6,15 +6,13 @@ import { useTranslation } from 'react-i18next'
import OutlineList from './outline-list' import OutlineList from './outline-list'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
function getChildrenLines(children) { const OutlineItem = memo(function OutlineItem({
return (children || []) outlineItem,
.map(child => { jumpToLine,
return getChildrenLines(child.children).concat(child.line) highlightedLine,
}) matchesHighlightedLine,
.flat() containsHighlightedLine,
} }) {
function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) {
const { t } = useTranslation() const { t } = useTranslation()
const [expanded, setExpanded] = useState(true) const [expanded, setExpanded] = useState(true)
@ -25,12 +23,8 @@ function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) {
'outline-item-no-children': !outlineItem.children, 'outline-item-no-children': !outlineItem.children,
}) })
const hasHighlightedChild = const hasHighlightedChild = !expanded && containsHighlightedLine
!expanded && const isHighlighted = matchesHighlightedLine || hasHighlightedChild
getChildrenLines(outlineItem.children).includes(highlightedLine)
const isHighlighted =
highlightedLine === outlineItem.line || hasHighlightedChild
const itemLinkClasses = classNames('outline-item-link', { const itemLinkClasses = classNames('outline-item-link', {
'outline-item-link-highlight': isHighlighted, 'outline-item-link-highlight': isHighlighted,
@ -90,16 +84,21 @@ function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) {
</button> </button>
</div> </div>
{expanded && outlineItem.children ? ( {expanded && outlineItem.children ? (
// highlightedLine is only provided to this list if the list contains
// the highlighted line. This means that whenever the list does not
// contain the highlighted line, the props provided to it are the same
// and the component can be memoized.
<OutlineList <OutlineList
outline={outlineItem.children} outline={outlineItem.children}
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
isRoot={false} isRoot={false}
highlightedLine={highlightedLine} highlightedLine={containsHighlightedLine ? highlightedLine : null}
containsHighlightedLine={containsHighlightedLine}
/> />
) : null} ) : null}
</li> </li>
) )
} })
OutlineItem.propTypes = { OutlineItem.propTypes = {
outlineItem: PropTypes.exact({ outlineItem: PropTypes.exact({
@ -113,6 +112,8 @@ OutlineItem.propTypes = {
}).isRequired, }).isRequired,
jumpToLine: PropTypes.func.isRequired, jumpToLine: PropTypes.func.isRequired,
highlightedLine: PropTypes.number, highlightedLine: PropTypes.number,
matchesHighlightedLine: PropTypes.bool,
containsHighlightedLine: PropTypes.bool,
} }
export default OutlineItem export default OutlineItem

View file

@ -1,32 +1,64 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classNames from 'classnames' import classNames from 'classnames'
import OutlineItem from './outline-item' import OutlineItem from './outline-item'
import { memo } from 'react'
function OutlineList({ outline, jumpToLine, isRoot, highlightedLine }) { function getChildrenLines(children) {
return (children || [])
.map(child => {
return getChildrenLines(child.children).concat(child.line)
})
.flat()
}
const OutlineList = memo(function OutlineList({
outline,
jumpToLine,
isRoot,
highlightedLine,
containsHighlightedLine,
}) {
const listClasses = classNames('outline-item-list', { const listClasses = classNames('outline-item-list', {
'outline-item-list-root': isRoot, 'outline-item-list-root': isRoot,
}) })
return ( return (
<ul className={listClasses} role={isRoot ? 'tree' : 'group'}> <ul className={listClasses} role={isRoot ? 'tree' : 'group'}>
{outline.map((outlineItem, idx) => { {outline.map((outlineItem, idx) => {
const matchesHighlightedLine =
containsHighlightedLine && highlightedLine === outlineItem.line
const itemContainsHighlightedLine =
containsHighlightedLine &&
getChildrenLines(outlineItem.children).includes(highlightedLine)
// highlightedLine is only provided to the item if the item matches or
// contains the highlighted line. This means that whenever the item does
// not contain the highlighted line, the props provided to it are the
// same and the component can be memoized.
return ( return (
<OutlineItem <OutlineItem
key={`${outlineItem.level}-${idx}`} key={`${outlineItem.level}-${idx}`}
outlineItem={outlineItem} outlineItem={outlineItem}
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
highlightedLine={highlightedLine} highlightedLine={
matchesHighlightedLine || itemContainsHighlightedLine
? highlightedLine
: null
}
matchesHighlightedLine={matchesHighlightedLine}
containsHighlightedLine={itemContainsHighlightedLine}
/> />
) )
})} })}
</ul> </ul>
) )
} })
OutlineList.propTypes = { OutlineList.propTypes = {
outline: PropTypes.array.isRequired, outline: PropTypes.array.isRequired,
jumpToLine: PropTypes.func.isRequired, jumpToLine: PropTypes.func.isRequired,
isRoot: PropTypes.bool, isRoot: PropTypes.bool,
highlightedLine: PropTypes.number, highlightedLine: PropTypes.number,
containsHighlightedLine: PropTypes.bool,
} }
export default OutlineList export default OutlineList

View file

@ -14,6 +14,7 @@ function OutlineRoot({ outline, jumpToLine, highlightedLine }) {
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
isRoot isRoot
highlightedLine={highlightedLine} highlightedLine={highlightedLine}
containsHighlightedLine
/> />
) : ( ) : (
<div className="outline-body-no-elements"> <div className="outline-body-no-elements">

View file

@ -54,6 +54,7 @@ describe('<OutlineItem />', function () {
outlineItem={outlineItem} outlineItem={outlineItem}
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
highlightedLine={1} highlightedLine={1}
matchesHighlightedLine
/> />
) )
@ -71,6 +72,7 @@ describe('<OutlineItem />', function () {
outlineItem={outlineItem} outlineItem={outlineItem}
jumpToLine={jumpToLine} jumpToLine={jumpToLine}
highlightedLine={2} highlightedLine={2}
containsHighlightedLine
/> />
) )