Merge pull request #3030 from overleaf/ta-outline-highlight

Outline Highlight

GitOrigin-RevId: f51eec6f1714403d749e454b55be363819930b98
This commit is contained in:
Timothée Alby
2020-08-10 15:54:08 +00:00
committed by Copybot
parent 52fafa96c5
commit 558854f51b
11 changed files with 127 additions and 31 deletions
@@ -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;