Merge pull request #3007 from overleaf/pr-file-outline-extras

File outline extras

GitOrigin-RevId: c35c4f35dce280c9f44c02b567df9734943d0cb0
This commit is contained in:
Paulo Jorge Reis
2020-07-17 02:06:42 +00:00
committed by Copybot
parent 79b6f6e473
commit 0e9771ac09
16 changed files with 341 additions and 146 deletions
@@ -13,6 +13,7 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../../base'
import _ from 'lodash'
import 'libs/jquery-layout'
import 'libs/jquery.ui.touch-punch'
@@ -233,9 +234,16 @@ ng-click=\"handleClick()\">\
}
// Save state when exiting
$(window).unload(() =>
ide.localStorage(`layout.${name}`, element.layout().readState())
)
$(window).unload(() => {
// Save only the state properties for the current layout, ignoring sublayouts inside it.
// If we save sublayouts state (`children`), the layout library will use it when
// initializing. This raises errors when the sublayout elements aren't available (due to
// being loaded at init or just not existing for the current project/user).
const stateToSave = _.mapValues(element.layout().readState(), pane =>
_.omit(pane, 'children')
)
ide.localStorage(`layout.${name}`, stateToSave)
})
if (attrs.openEast != null) {
scope.$watch(attrs.openEast, function(value, oldValue) {
@@ -0,0 +1,93 @@
import App from '../../base'
const layoutOptions = {
center: {
paneSelector: '[vertical-resizable-top]',
paneClass: 'vertical-resizable-top',
size: 'auto'
},
south: {
paneSelector: '[vertical-resizable-bottom]',
paneClass: 'vertical-resizable-bottom',
resizerClass: 'vertical-resizable-resizer',
resizerCursor: 'row-resize',
size: 'auto',
resizable: true,
closable: false,
slidable: false,
spacing_open: 6,
spacing_closed: 6,
maxSize: '75%'
}
}
export default App.directive('verticalResizablePanes', localStorage => ({
restrict: 'A',
link(scope, element, attrs) {
let name = attrs.verticalResizablePanes
let storedSize = null
let manualResizeIncoming = false
if (name) {
const storageKey = `vertical-resizable:${name}:south-size`
storedSize = localStorage(storageKey)
$(window).unload(() => {
if (storedSize) {
localStorage(storageKey, storedSize)
}
})
}
const toggledExternally = attrs.verticalResizablePanesToggledExternallyOn
const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
function enableResizer() {
if (layoutHandle.resizers && layoutHandle.resizers.south) {
layoutHandle.resizers.south.removeClass(resizerDisabledClass)
}
}
function disableResizer() {
if (layoutHandle.resizers && layoutHandle.resizers.south) {
layoutHandle.resizers.south.addClass(resizerDisabledClass)
}
}
function handleDragEnd() {
manualResizeIncoming = true
}
function handleResize(paneName, paneEl, paneState) {
if (manualResizeIncoming) {
storedSize = paneState.size
}
manualResizeIncoming = false
}
if (toggledExternally) {
scope.$on(toggledExternally, (e, open) => {
let newSize = 'auto'
if (open) {
if (storedSize) {
newSize = storedSize
}
enableResizer()
} else {
disableResizer()
}
layoutHandle.sizePane('south', newSize)
})
}
// The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when
// the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to
// listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we
// get the pane size *before* the resize happens. We do get the correct size in the next `resize` event.
// The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event
// that it was user-initiated (therefore, storing the value).
layoutOptions.south.ondrag_end = handleDragEnd
layoutOptions.south.onresize = handleResize
const layoutHandle = element.layout(layoutOptions)
}
}))
@@ -132,6 +132,7 @@ export default (EditorManager = (function() {
this.$scope.ui.view = 'editor'
const done = () => {
this.$scope.$broadcast('doc:after-opened')
if (options.gotoLine != null) {
// allow Ace to display document before moving, delay until next tick
// added delay to make this happen later that gotoStoredPosition in
@@ -14,8 +14,8 @@ class OutlineManager {
this.isTexFile = false
this.outline = []
scope.$watch('editor.sharejs_doc', shareJsDoc => {
this.shareJsDoc = shareJsDoc
scope.$on('doc:after-opened', () => {
this.shareJsDoc = scope.editor.shareJsDoc
this.isTexFile = isValidTeXFile(scope.editor.open_doc_name)
this.updateOutline()
this.broadcastChangeEvent()
@@ -1,15 +1,28 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import classNames from 'classnames'
import OutlineRoot from './OutlineRoot'
import localStorage from '../../../modules/localStorage'
function OutlinePane({ isTexFile, outline, jumpToLine }) {
const [expanded, setExpanded] = useState(true)
function OutlinePane({ isTexFile, outline, projectId, jumpToLine, onToggle }) {
const storageKey = `file_outline.expanded.${projectId}`
const [expanded, setExpanded] = useState(() => {
const storedExpandedState = localStorage(storageKey) !== false
return storedExpandedState
})
const isOpen = isTexFile && expanded
useEffect(
() => {
onToggle(isOpen)
},
[isOpen]
)
const expandCollapseIconClasses = classNames('fa', 'outline-caret-icon', {
'fa-angle-down': expanded,
'fa-angle-right': !expanded
'fa-angle-down': isOpen,
'fa-angle-right': !isOpen
})
const headerClasses = classNames('outline-pane', {
@@ -18,6 +31,7 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
function handleExpandCollapseClick() {
if (isTexFile) {
localStorage(storageKey, !expanded)
setExpanded(!expanded)
}
}
@@ -32,20 +46,20 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
>
<i className={expandCollapseIconClasses} />
<h4 className="outline-header-name">File outline</h4>
<OverlayTrigger placement="top" overlay={tooltip} delayHide={100}>
<a
href="/beta/participate"
target="_blank"
rel="noopener noreferrer"
className="outline-header-beta-badge"
>
<span className="sr-only">
The File outline is a beta feature. Click here to manage your
beta program membership.
</span>
</a>
</OverlayTrigger>
</button>
<OverlayTrigger placement="top" overlay={tooltip} delayHide={100}>
<a
href="/beta/participate"
target="_blank"
rel="noopener noreferrer"
className="outline-header-beta-badge"
>
<span className="sr-only">
The File outline is a beta feature. Click here to manage your beta
program membership.
</span>
</a>
</OverlayTrigger>
</header>
{expanded && isTexFile ? (
<div className="outline-body">
@@ -65,7 +79,9 @@ const tooltip = (
OutlinePane.propTypes = {
isTexFile: PropTypes.bool.isRequired,
outline: PropTypes.array.isRequired,
jumpToLine: PropTypes.func.isRequired
projectId: PropTypes.string.isRequired,
jumpToLine: PropTypes.func.isRequired,
onToggle: PropTypes.func
}
export default OutlinePane
@@ -18,10 +18,22 @@ App.controller('OutlineController', function($scope, ide) {
$scope.jumpToLine = lineNo => {
ide.outlineManager.jumpToLine(lineNo)
}
$scope.onToggle = isOpen => {
$scope.$applyAsync(() => {
$scope.$emit('outline-toggled', isOpen)
})
}
})
// Wrap React component as Angular component. Only needed for "top-level" component
App.component(
'outlinePane',
react2angular(OutlinePane, ['outline', 'jumpToLine', 'isTexFile'])
react2angular(OutlinePane, [
'outline',
'jumpToLine',
'projectId',
'isTexFile',
'onToggle'
])
)