mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
Merge pull request #3030 from overleaf/ta-outline-highlight
Outline Highlight GitOrigin-RevId: f51eec6f1714403d749e454b55be363819930b98
This commit is contained in:
@@ -111,6 +111,7 @@ aside.editor-sidebar.full-size(
|
||||
jump-to-line="jumpToLine"
|
||||
on-toggle="onToggle"
|
||||
event-tracking="eventTracking"
|
||||
highlighted-line="highlightedLine"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -124,6 +124,14 @@ export default (EditorManager = (function() {
|
||||
return this.openDoc(doc, options)
|
||||
}
|
||||
|
||||
jumpToLine(options) {
|
||||
return this.$scope.$broadcast(
|
||||
'editor:gotoLine',
|
||||
options.gotoLine,
|
||||
options.gotoColumn
|
||||
)
|
||||
}
|
||||
|
||||
openDoc(doc, options) {
|
||||
if (options == null) {
|
||||
options = {}
|
||||
@@ -137,13 +145,7 @@ export default (EditorManager = (function() {
|
||||
// allow Ace to display document before moving, delay until next tick
|
||||
// added delay to make this happen later that gotoStoredPosition in
|
||||
// CursorPositionManager
|
||||
return setTimeout(() => {
|
||||
return this.$scope.$broadcast(
|
||||
'editor:gotoLine',
|
||||
options.gotoLine,
|
||||
options.gotoColumn
|
||||
)
|
||||
}, 0)
|
||||
return setTimeout(() => this.jumpToLine(options), 0)
|
||||
} else if (options.gotoOffset != null) {
|
||||
return setTimeout(() => {
|
||||
return this.$scope.$broadcast(
|
||||
|
||||
@@ -554,11 +554,26 @@ App.directive('aceEditor', function(
|
||||
return scope.$emit(`${scope.name}:change`)
|
||||
}
|
||||
|
||||
let currentFirstVisibleRow = null
|
||||
const emitMiddleVisibleRowChanged = () => {
|
||||
if (!window.user.alphaProgram) return
|
||||
const firstVisibleRow = editor.getFirstVisibleRow()
|
||||
if (firstVisibleRow === currentFirstVisibleRow) return
|
||||
|
||||
currentFirstVisibleRow = firstVisibleRow
|
||||
const lastVisibleRow = editor.getLastVisibleRow()
|
||||
scope.$emit(
|
||||
`scroll:editor:update`,
|
||||
Math.floor((firstVisibleRow + lastVisibleRow) / 2)
|
||||
)
|
||||
}
|
||||
|
||||
const onScroll = function(scrollTop) {
|
||||
if (scope.eventsBridge == null) {
|
||||
return
|
||||
}
|
||||
const height = editor.renderer.layerConfig.maxHeight
|
||||
emitMiddleVisibleRowChanged()
|
||||
return scope.eventsBridge.emit('aceScroll', scrollTop, height)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import './components/OutlinePane'
|
||||
import './components/OutlineRoot'
|
||||
import './components/OutlineList'
|
||||
import './components/OutlineItem'
|
||||
import parseOutline from './OutlineParser'
|
||||
import { matchOutline, nestOutline } from './OutlineParser'
|
||||
import isValidTeXFile from '../../main/is-valid-tex-file'
|
||||
|
||||
class OutlineManager {
|
||||
@@ -12,9 +12,15 @@ class OutlineManager {
|
||||
this.scope = scope
|
||||
this.shareJsDoc = null
|
||||
this.isTexFile = false
|
||||
this.flatOutline = []
|
||||
this.outline = []
|
||||
this.highlightedLine = null
|
||||
this.ignoreNextScroll = false
|
||||
this.ignoreNextCursorUpdate = false
|
||||
|
||||
scope.$on('doc:after-opened', () => {
|
||||
this.ignoreNextScroll = true
|
||||
this.ignoreNextCursorUpdate = true
|
||||
this.shareJsDoc = scope.editor.sharejs_doc
|
||||
this.isTexFile = isValidTeXFile(scope.editor.open_doc_name)
|
||||
this.updateOutline()
|
||||
@@ -31,6 +37,32 @@ class OutlineManager {
|
||||
this.updateOutline()
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$on('cursor:editor:update', (event, cursorPosition) => {
|
||||
if (!window.user.alphaProgram) return
|
||||
if (this.ignoreNextCursorUpdate) {
|
||||
this.ignoreNextCursorUpdate = false
|
||||
return
|
||||
}
|
||||
this.updateHighlightedLine(cursorPosition.row + 1)
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$on('scroll:editor:update', (event, middleVisibleRow) => {
|
||||
if (!window.user.alphaProgram) return
|
||||
if (this.ignoreNextScroll) {
|
||||
this.ignoreNextScroll = false
|
||||
return
|
||||
}
|
||||
|
||||
this.updateHighlightedLine(middleVisibleRow + 1)
|
||||
this.broadcastChangeEvent()
|
||||
})
|
||||
|
||||
scope.$watch('editor.showRichText', () => {
|
||||
this.ignoreNextScroll = true
|
||||
this.ignoreNextCursorUpdate = true
|
||||
})
|
||||
}
|
||||
|
||||
updateOutline() {
|
||||
@@ -38,21 +70,33 @@ class OutlineManager {
|
||||
if (this.isTexFile) {
|
||||
const content = this.ide.editorManager.getCurrentDocValue()
|
||||
if (content) {
|
||||
this.outline = parseOutline(content)
|
||||
this.flatOutline = matchOutline(content)
|
||||
this.outline = nestOutline(this.flatOutline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set highlightedLine to the closest outline line above the editorLine
|
||||
updateHighlightedLine(editorLine) {
|
||||
let closestOutlineLine = null
|
||||
for (let lineId = 0; lineId < this.flatOutline.length; lineId++) {
|
||||
const outline = this.flatOutline[lineId]
|
||||
if (editorLine < outline.line) break // editorLine is above
|
||||
closestOutlineLine = outline.line
|
||||
}
|
||||
this.highlightedLine = closestOutlineLine
|
||||
}
|
||||
|
||||
jumpToLine(line) {
|
||||
this.ide.editorManager.openDocId(this.shareJsDoc.doc.doc_id, {
|
||||
gotoLine: line
|
||||
})
|
||||
this.ignoreNextScroll = true
|
||||
this.ide.editorManager.jumpToLine({ gotoLine: line })
|
||||
}
|
||||
|
||||
broadcastChangeEvent() {
|
||||
this.scope.$broadcast('outline-manager:outline-changed', {
|
||||
isTexFile: this.isTexFile,
|
||||
outline: this.outline
|
||||
outline: this.outline,
|
||||
highlightedLine: this.highlightedLine
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,9 +113,4 @@ function nestOutline(flatOutline) {
|
||||
return nestedOutlines
|
||||
}
|
||||
|
||||
function parseOutline(content) {
|
||||
const flatOutline = matchOutline(content)
|
||||
return nestOutline(flatOutline)
|
||||
}
|
||||
|
||||
export default parseOutline
|
||||
export { matchOutline, nestOutline }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect, createRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import OutlineList from './OutlineList'
|
||||
|
||||
function OutlineItem({ outlineItem, jumpToLine }) {
|
||||
function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const titleElementRef = createRef()
|
||||
|
||||
const mainItemClasses = classNames('outline-item', {
|
||||
'outline-item-no-children': !outlineItem.children
|
||||
@@ -15,6 +16,10 @@ function OutlineItem({ outlineItem, jumpToLine }) {
|
||||
'fa-angle-right': !expanded
|
||||
})
|
||||
|
||||
const itemLinkClasses = classNames('outline-item-link', {
|
||||
'outline-item-link-highlight': highlightedLine === outlineItem.line
|
||||
})
|
||||
|
||||
function handleExpandCollapseClick() {
|
||||
setExpanded(!expanded)
|
||||
}
|
||||
@@ -23,6 +28,18 @@ function OutlineItem({ outlineItem, jumpToLine }) {
|
||||
jumpToLine(outlineItem.line)
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (highlightedLine !== outlineItem.line) return
|
||||
|
||||
titleElementRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
},
|
||||
[highlightedLine, titleElementRef]
|
||||
)
|
||||
|
||||
return (
|
||||
<li className={mainItemClasses}>
|
||||
<div className="outline-item-row">
|
||||
@@ -35,8 +52,9 @@ function OutlineItem({ outlineItem, jumpToLine }) {
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
className="outline-item-link"
|
||||
className={itemLinkClasses}
|
||||
onClick={handleOutlineItemLinkClick}
|
||||
ref={titleElementRef}
|
||||
>
|
||||
{outlineItem.title}
|
||||
</button>
|
||||
@@ -46,6 +64,7 @@ function OutlineItem({ outlineItem, jumpToLine }) {
|
||||
outline={outlineItem.children}
|
||||
jumpToLine={jumpToLine}
|
||||
isRoot={false}
|
||||
highlightedLine={highlightedLine}
|
||||
/>
|
||||
) : null}
|
||||
</li>
|
||||
@@ -59,7 +78,8 @@ OutlineItem.propTypes = {
|
||||
level: PropTypes.number.isRequired,
|
||||
children: PropTypes.array
|
||||
}).isRequired,
|
||||
jumpToLine: PropTypes.func.isRequired
|
||||
jumpToLine: PropTypes.func.isRequired,
|
||||
highlightedLine: PropTypes.number
|
||||
}
|
||||
|
||||
export default OutlineItem
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import OutlineItem from './OutlineItem'
|
||||
|
||||
function OutlineList({ outline, jumpToLine, isRoot }) {
|
||||
function OutlineList({ outline, jumpToLine, isRoot, highlightedLine }) {
|
||||
const listClasses = classNames('outline-item-list', {
|
||||
'outline-item-list-root': isRoot
|
||||
})
|
||||
@@ -15,6 +15,7 @@ function OutlineList({ outline, jumpToLine, isRoot }) {
|
||||
key={`${outlineItem.level}-${idx}`}
|
||||
outlineItem={outlineItem}
|
||||
jumpToLine={jumpToLine}
|
||||
highlightedLine={highlightedLine}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -25,7 +26,8 @@ function OutlineList({ outline, jumpToLine, isRoot }) {
|
||||
OutlineList.propTypes = {
|
||||
outline: PropTypes.array.isRequired,
|
||||
jumpToLine: PropTypes.func.isRequired,
|
||||
isRoot: PropTypes.bool
|
||||
isRoot: PropTypes.bool,
|
||||
highlightedLine: PropTypes.number
|
||||
}
|
||||
|
||||
export default OutlineList
|
||||
|
||||
@@ -11,7 +11,8 @@ function OutlinePane({
|
||||
projectId,
|
||||
jumpToLine,
|
||||
onToggle,
|
||||
eventTracking
|
||||
eventTracking,
|
||||
highlightedLine
|
||||
}) {
|
||||
const storageKey = `file_outline.expanded.${projectId}`
|
||||
const [expanded, setExpanded] = useState(() => {
|
||||
@@ -71,7 +72,11 @@ function OutlinePane({
|
||||
</header>
|
||||
{expanded && isTexFile ? (
|
||||
<div className="outline-body">
|
||||
<OutlineRoot outline={outline} jumpToLine={jumpToLine} />
|
||||
<OutlineRoot
|
||||
outline={outline}
|
||||
jumpToLine={jumpToLine}
|
||||
highlightedLine={highlightedLine}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -90,7 +95,8 @@ OutlinePane.propTypes = {
|
||||
projectId: PropTypes.string.isRequired,
|
||||
jumpToLine: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func,
|
||||
eventTracking: PropTypes.object.isRequired
|
||||
eventTracking: PropTypes.object.isRequired,
|
||||
highlightedLine: PropTypes.number
|
||||
}
|
||||
|
||||
export default OutlinePane
|
||||
|
||||
@@ -2,11 +2,16 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import OutlineList from './OutlineList'
|
||||
|
||||
function OutlineRoot({ outline, jumpToLine }) {
|
||||
function OutlineRoot({ outline, jumpToLine, highlightedLine }) {
|
||||
return (
|
||||
<div>
|
||||
{outline.length ? (
|
||||
<OutlineList outline={outline} jumpToLine={jumpToLine} isRoot />
|
||||
<OutlineList
|
||||
outline={outline}
|
||||
jumpToLine={jumpToLine}
|
||||
isRoot
|
||||
highlightedLine={highlightedLine}
|
||||
/>
|
||||
) : (
|
||||
<div className="outline-body-no-elements">
|
||||
We can’t find any sections or subsections in this file.{' '}
|
||||
@@ -26,7 +31,8 @@ function OutlineRoot({ outline, jumpToLine }) {
|
||||
|
||||
OutlineRoot.propTypes = {
|
||||
outline: PropTypes.array.isRequired,
|
||||
jumpToLine: PropTypes.func.isRequired
|
||||
jumpToLine: PropTypes.func.isRequired,
|
||||
highlightedLine: PropTypes.number
|
||||
}
|
||||
|
||||
export default OutlineRoot
|
||||
|
||||
@@ -13,6 +13,7 @@ App.controller('OutlineController', function($scope, ide, eventTracking) {
|
||||
$scope.$applyAsync(() => {
|
||||
$scope.isTexFile = outlineInfo.isTexFile
|
||||
$scope.outline = outlineInfo.outline
|
||||
$scope.highlightedLine = outlineInfo.highlightedLine
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.outline-item-link-highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.outline-caret-icon {
|
||||
width: @outline-h-rhythm;
|
||||
font-size: 17px;
|
||||
|
||||
Reference in New Issue
Block a user