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-16 11:09:01 +01:00 committed by Copybot
parent 79b6f6e473
commit 0e9771ac09
16 changed files with 341 additions and 146 deletions

View file

@ -1,4 +1,13 @@
aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }", ng-show="ui.view != 'history' || !history.isV2").full-size aside.editor-sidebar.full-size(
ng-controller="FileTreeController"
ng-class="{ 'multi-selected': multiSelectedCount > 0 }"
ng-show="ui.view != 'history' || !history.isV2"
vertical-resizable-panes=user.alphaProgram && "outline-resizer"
vertical-resizable-panes-toggled-externally-on=user.alphaProgram && "outline-toggled"
)
.file-tree(
vertical-resizable-top=user.alphaProgram
)
.toolbar.toolbar-filetree(ng-if="permissions.write") .toolbar.toolbar-filetree(ng-if="permissions.write")
a( a(
href, href,
@ -88,15 +97,17 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
span {{ entity.name }} span {{ entity.name }}
if user.alphaProgram if user.alphaProgram
.outline-container( .outline-container(
vertical-resizable-bottom
ng-controller="OutlineController" ng-controller="OutlineController"
) )
outline-pane( outline-pane(
is-tex-file="isTexFile" is-tex-file="isTexFile"
outline="outline" outline="outline"
project-id="project_id"
jump-to-line="jumpToLine" jump-to-line="jumpToLine"
on-toggle="onToggle"
) )

View file

@ -1,4 +1,4 @@
aside.file-tree.full-size( aside.editor-sidebar.full-size(
ng-controller="HistoryV2FileTreeController" ng-controller="HistoryV2FileTreeController"
ng-if="ui.view == 'history' && history.isV2" ng-if="ui.view == 'history' && history.isV2"
) )

View file

@ -40,6 +40,7 @@ import './ide/hotkeys/index'
import './ide/wordcount/index' import './ide/wordcount/index'
import './ide/directives/layout' import './ide/directives/layout'
import './ide/directives/validFile' import './ide/directives/validFile'
import './ide/directives/verticalResizablePanes'
import './ide/services/ide' import './ide/services/ide'
import './directives/focus' import './directives/focus'
import './directives/fineUpload' import './directives/fineUpload'

View file

@ -13,6 +13,7 @@
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/ */
import App from '../../base' import App from '../../base'
import _ from 'lodash'
import 'libs/jquery-layout' import 'libs/jquery-layout'
import 'libs/jquery.ui.touch-punch' import 'libs/jquery.ui.touch-punch'
@ -233,9 +234,16 @@ ng-click=\"handleClick()\">\
} }
// Save state when exiting // Save state when exiting
$(window).unload(() => $(window).unload(() => {
ide.localStorage(`layout.${name}`, element.layout().readState()) // 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) { if (attrs.openEast != null) {
scope.$watch(attrs.openEast, function(value, oldValue) { scope.$watch(attrs.openEast, function(value, oldValue) {

View file

@ -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)
}
}))

View file

@ -132,6 +132,7 @@ export default (EditorManager = (function() {
this.$scope.ui.view = 'editor' this.$scope.ui.view = 'editor'
const done = () => { const done = () => {
this.$scope.$broadcast('doc:after-opened')
if (options.gotoLine != null) { if (options.gotoLine != null) {
// allow Ace to display document before moving, delay until next tick // allow Ace to display document before moving, delay until next tick
// added delay to make this happen later that gotoStoredPosition in // added delay to make this happen later that gotoStoredPosition in

View file

@ -14,8 +14,8 @@ class OutlineManager {
this.isTexFile = false this.isTexFile = false
this.outline = [] this.outline = []
scope.$watch('editor.sharejs_doc', shareJsDoc => { scope.$on('doc:after-opened', () => {
this.shareJsDoc = shareJsDoc this.shareJsDoc = scope.editor.shareJsDoc
this.isTexFile = isValidTeXFile(scope.editor.open_doc_name) this.isTexFile = isValidTeXFile(scope.editor.open_doc_name)
this.updateOutline() this.updateOutline()
this.broadcastChangeEvent() this.broadcastChangeEvent()

View file

@ -1,15 +1,28 @@
import React, { useState } from 'react' import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { OverlayTrigger, Tooltip } from 'react-bootstrap' import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import classNames from 'classnames' import classNames from 'classnames'
import OutlineRoot from './OutlineRoot' import OutlineRoot from './OutlineRoot'
import localStorage from '../../../modules/localStorage'
function OutlinePane({ isTexFile, outline, jumpToLine }) { function OutlinePane({ isTexFile, outline, projectId, jumpToLine, onToggle }) {
const [expanded, setExpanded] = useState(true) 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', { const expandCollapseIconClasses = classNames('fa', 'outline-caret-icon', {
'fa-angle-down': expanded, 'fa-angle-down': isOpen,
'fa-angle-right': !expanded 'fa-angle-right': !isOpen
}) })
const headerClasses = classNames('outline-pane', { const headerClasses = classNames('outline-pane', {
@ -18,6 +31,7 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
function handleExpandCollapseClick() { function handleExpandCollapseClick() {
if (isTexFile) { if (isTexFile) {
localStorage(storageKey, !expanded)
setExpanded(!expanded) setExpanded(!expanded)
} }
} }
@ -32,7 +46,6 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
> >
<i className={expandCollapseIconClasses} /> <i className={expandCollapseIconClasses} />
<h4 className="outline-header-name">File outline</h4> <h4 className="outline-header-name">File outline</h4>
</button>
<OverlayTrigger placement="top" overlay={tooltip} delayHide={100}> <OverlayTrigger placement="top" overlay={tooltip} delayHide={100}>
<a <a
href="/beta/participate" href="/beta/participate"
@ -41,11 +54,12 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
className="outline-header-beta-badge" className="outline-header-beta-badge"
> >
<span className="sr-only"> <span className="sr-only">
The File outline is a beta feature. Click here to manage your beta The File outline is a beta feature. Click here to manage your
program membership. beta program membership.
</span> </span>
</a> </a>
</OverlayTrigger> </OverlayTrigger>
</button>
</header> </header>
{expanded && isTexFile ? ( {expanded && isTexFile ? (
<div className="outline-body"> <div className="outline-body">
@ -65,7 +79,9 @@ const tooltip = (
OutlinePane.propTypes = { OutlinePane.propTypes = {
isTexFile: PropTypes.bool.isRequired, isTexFile: PropTypes.bool.isRequired,
outline: PropTypes.array.isRequired, outline: PropTypes.array.isRequired,
jumpToLine: PropTypes.func.isRequired projectId: PropTypes.string.isRequired,
jumpToLine: PropTypes.func.isRequired,
onToggle: PropTypes.func
} }
export default OutlinePane export default OutlinePane

View file

@ -18,10 +18,22 @@ App.controller('OutlineController', function($scope, ide) {
$scope.jumpToLine = lineNo => { $scope.jumpToLine = lineNo => {
ide.outlineManager.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 // Wrap React component as Angular component. Only needed for "top-level" component
App.component( App.component(
'outlinePane', 'outlinePane',
react2angular(OutlinePane, ['outline', 'jumpToLine', 'isTexFile']) react2angular(OutlinePane, [
'outline',
'jumpToLine',
'projectId',
'isTexFile',
'onToggle'
])
) )

View file

@ -1,13 +1,17 @@
angular.module('localStorage', []).value('localStorage', function(...args) { angular.module('localStorage', []).value('localStorage', localStorage)
/*
/*
localStorage can throw browser exceptions, for example if it is full localStorage can throw browser exceptions, for example if it is full
We don't use localStorage for anything critical, on in that case just We don't use localStorage for anything critical, on in that case just
fail gracefully. fail gracefully.
*/ */
function localStorage(...args) {
try { try {
return $.localStorage(...args) return $.localStorage(...args)
} catch (e) { } catch (e) {
console.error('localStorage exception', e) console.error('localStorage exception', e)
return null return null
} }
}) }
export default localStorage

View file

@ -61,6 +61,7 @@
@import 'components/popovers.less'; @import 'components/popovers.less';
@import 'components/carousel.less'; @import 'components/carousel.less';
@import 'components/daterange-picker'; @import 'components/daterange-picker';
@import 'components/vertical-resizable-panes.less';
// ngTagsInput // ngTagsInput
@import 'components/tags-input.less'; @import 'components/tags-input.less';

View file

@ -8,9 +8,16 @@
} }
} }
.file-tree { .editor-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
}
.file-tree {
display: flex !important; // To work around jQuery layout's inline styles
flex-direction: column;
flex-grow: 1;
max-height: 100%;
.toolbar.toolbar-filetree { .toolbar.toolbar-filetree {
.toolbar-small-mixin; .toolbar-small-mixin;

View file

@ -1,10 +1,5 @@
@outline-v-rhythm: 24px;
@outline-h-rhythm: 24px;
@outline-item-h-padding: @outline-h-rhythm * 0.25;
.outline-container { .outline-container {
width: 100%; width: 100%;
max-height: 50%; // While we don't support resizing.
background-color: @file-tree-bg; background-color: @file-tree-bg;
} }
@ -13,7 +8,7 @@
flex-flow: column; flex-flow: column;
height: 100%; height: 100%;
font-size: @font-size-small; font-size: @font-size-small;
color: #fff; color: @file-tree-item-color;
} }
.outline-pane-disabled { .outline-pane-disabled {
@ -24,29 +19,33 @@
.toolbar-small-mixin; .toolbar-small-mixin;
.toolbar-alt-mixin; .toolbar-alt-mixin;
display: flex; display: flex;
align-items: center;
padding-right: @outline-h-rhythm * 0.25;
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid @toolbar-border-color;
border-top: 1px solid @toolbar-border-color;
} }
.outline-header-expand-collapse-btn { .outline-header-expand-collapse-btn {
display: inline-block; display: flex;
align-items: center;
background-color: transparent; background-color: transparent;
border: 0; border: 0;
padding: 0; padding: 0 (@outline-h-rhythm * 0.25) 0 0;
font-size: inherit; font-size: inherit;
vertical-align: inherit; vertical-align: inherit;
color: #fff; color: @file-tree-item-color;
flex-grow: 1; flex: 1 0 100%;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
overflow: hidden; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
border-radius: @border-radius-base;
text-overflow: ellipsis;
&:hover, &:hover,
&:focus { &:focus {
outline: 0; outline: 0;
background-color: @ol-blue-gray-5; }
&:hover {
background-color: @outline-header-hover-bg;
}
&:hover[disabled] {
background-color: transparent;
} }
} }
@ -54,8 +53,13 @@
display: inline-block; display: inline-block;
font-family: @font-family-sans-serif; font-family: @font-family-sans-serif;
font-size: @font-size-small; font-size: @font-size-small;
color: #fff; color: @file-tree-item-color;
font-weight: 700;
margin: 0; margin: 0;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
} }
.outline-header-beta-badge { .outline-header-beta-badge {
@ -87,7 +91,7 @@
} }
.outline-body-no-elements { .outline-body-no-elements {
color: @ol-blue-gray-2; color: @outline-no-items-color;
text-align: center; text-align: center;
padding: @outline-v-rhythm @outline-h-rhythm (@outline-v-rhythm * 2); padding: @outline-v-rhythm @outline-h-rhythm (@outline-v-rhythm * 2);
margin-right: -(@outline-h-rhythm * 0.25); margin-right: -(@outline-h-rhythm * 0.25);
@ -95,11 +99,11 @@
.outline-body-link { .outline-body-link {
display: block; display: block;
color: #fff; color: @file-tree-item-color;
text-decoration: underline; text-decoration: underline;
&:hover, &:hover,
&:focus { &:focus {
color: #fff; color: @file-tree-item-color;
text-decoration: underline; text-decoration: underline;
} }
} }
@ -111,7 +115,7 @@
&::before { &::before {
content: ''; content: '';
background-color: @ol-blue-gray-3; background-color: @outline-line-guide-color;
top: @outline-h-rhythm / 4; top: @outline-h-rhythm / 4;
bottom: @outline-h-rhythm / 4; bottom: @outline-h-rhythm / 4;
width: 1px; width: 1px;
@ -127,9 +131,6 @@
} }
} }
.outline-item {
}
.outline-item-no-children { .outline-item-no-children {
padding-left: @outline-h-rhythm - @outline-item-h-padding; padding-left: @outline-h-rhythm - @outline-item-h-padding;
} }
@ -149,19 +150,19 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
background-color: @file-tree-bg; background-color: @file-tree-bg;
color: @ol-blue-gray-2; color: @outline-expand-collapse-color;
margin-right: -(@outline-item-h-padding); margin-right: -(@outline-item-h-padding);
border-radius: @border-radius-base; border-radius: @border-radius-base;
&:hover, &:hover,
&:focus { &:focus {
outline: 0; outline: 0;
background-color: @ol-blue-gray-5; background-color: @file-tree-item-hover-bg;
} }
} }
.outline-item-link { .outline-item-link {
display: inline; display: inline;
color: #fff; color: @file-tree-item-color;
background-color: transparent; background-color: transparent;
border: 0; border: 0;
position: relative; position: relative;
@ -174,7 +175,7 @@
&:hover, &:hover,
&:focus { &:focus {
outline: 0; outline: 0;
background-color: @ol-blue-gray-5; background-color: @file-tree-item-hover-bg;
} }
} }

View file

@ -0,0 +1,21 @@
.vertical-resizable-resizer {
background-color: @vertical-resizable-resizer-bg;
&:hover {
background-color: @vertical-resizable-resizer-hover-bg;
}
&::after {
content: '····';
display: block;
color: @ol-blue-gray-2;
text-align: center;
font-size: 20px;
line-height: 3px;
pointer-events: none;
}
}
.vertical-resizable-resizer-disabled {
pointer-events: none;
}

View file

@ -57,6 +57,13 @@
@content-alt-bg-color : @ol-blue-gray-0; @content-alt-bg-color : @ol-blue-gray-0;
// File outline
@outline-line-guide-color: @ol-blue-gray-1;
@outline-header-hover-bg: @file-tree-item-hover-bg;
@vertical-resizable-resizer-bg: @ol-blue-gray-1;
@vertical-resizable-resizer-hover-bg: @file-tree-item-hover-bg;
// Editor resizers // Editor resizers
@editor-resizer-bg-color : @ol-blue-gray-1; @editor-resizer-bg-color : @ol-blue-gray-1;
@editor-resizer-bg-color-dragging: @ol-blue-gray-1; @editor-resizer-bg-color-dragging: @ol-blue-gray-1;

View file

@ -1012,6 +1012,18 @@
@file-tree-multiselect-hover-bg: @ol-dark-blue; @file-tree-multiselect-hover-bg: @ol-dark-blue;
@file-tree-droppable-bg-color: @ol-blue-gray-2; @file-tree-droppable-bg-color: @ol-blue-gray-2;
// File outline
@outline-v-rhythm: 24px;
@outline-h-rhythm: 24px;
@outline-item-h-padding: @outline-h-rhythm * 0.25;
@outline-line-guide-color: @ol-blue-gray-3;
@outline-expand-collapse-color: @ol-blue-gray-2;
@outline-no-items-color: @ol-blue-gray-2;
@outline-header-hover-bg: @ol-blue-gray-6;
@vertical-resizable-resizer-bg: @ol-blue-gray-5;
@vertical-resizable-resizer-hover-bg: @ol-blue-gray-6;
// Editor resizers // Editor resizers
@editor-resizer-bg-color: @ol-blue-gray-5; @editor-resizer-bg-color: @ol-blue-gray-5;
@editor-resizer-bg-color-dragging: @ol-blue-gray-5; @editor-resizer-bg-color-dragging: @ol-blue-gray-5;