mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #3007 from overleaf/pr-file-outline-extras
File outline extras GitOrigin-RevId: c35c4f35dce280c9f44c02b567df9734943d0cb0
This commit is contained in:
parent
79b6f6e473
commit
0e9771ac09
16 changed files with 341 additions and 146 deletions
|
@ -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")
|
||||
a(
|
||||
href,
|
||||
|
@ -88,15 +97,17 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
|
|||
|
||||
span {{ entity.name }}
|
||||
|
||||
|
||||
if user.alphaProgram
|
||||
.outline-container(
|
||||
vertical-resizable-bottom
|
||||
ng-controller="OutlineController"
|
||||
)
|
||||
outline-pane(
|
||||
is-tex-file="isTexFile"
|
||||
outline="outline"
|
||||
project-id="project_id"
|
||||
jump-to-line="jumpToLine"
|
||||
on-toggle="onToggle"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
aside.file-tree.full-size(
|
||||
aside.editor-sidebar.full-size(
|
||||
ng-controller="HistoryV2FileTreeController"
|
||||
ng-if="ui.view == 'history' && history.isV2"
|
||||
)
|
||||
|
|
|
@ -40,6 +40,7 @@ import './ide/hotkeys/index'
|
|||
import './ide/wordcount/index'
|
||||
import './ide/directives/layout'
|
||||
import './ide/directives/validFile'
|
||||
import './ide/directives/verticalResizablePanes'
|
||||
import './ide/services/ide'
|
||||
import './directives/focus'
|
||||
import './directives/fineUpload'
|
||||
|
|
|
@ -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,7 +46,6 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
|
|||
>
|
||||
<i className={expandCollapseIconClasses} />
|
||||
<h4 className="outline-header-name">File outline</h4>
|
||||
</button>
|
||||
<OverlayTrigger placement="top" overlay={tooltip} delayHide={100}>
|
||||
<a
|
||||
href="/beta/participate"
|
||||
|
@ -41,11 +54,12 @@ function OutlinePane({ isTexFile, outline, jumpToLine }) {
|
|||
className="outline-header-beta-badge"
|
||||
>
|
||||
<span className="sr-only">
|
||||
The File outline is a beta feature. Click here to manage your beta
|
||||
program membership.
|
||||
The File outline is a beta feature. Click here to manage your
|
||||
beta program membership.
|
||||
</span>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</button>
|
||||
</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'
|
||||
])
|
||||
)
|
||||
|
|
|
@ -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
|
||||
We don't use localStorage for anything critical, on in that case just
|
||||
fail gracefully.
|
||||
*/
|
||||
*/
|
||||
function localStorage(...args) {
|
||||
try {
|
||||
return $.localStorage(...args)
|
||||
} catch (e) {
|
||||
console.error('localStorage exception', e)
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default localStorage
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
@import 'components/popovers.less';
|
||||
@import 'components/carousel.less';
|
||||
@import 'components/daterange-picker';
|
||||
@import 'components/vertical-resizable-panes.less';
|
||||
|
||||
// ngTagsInput
|
||||
@import 'components/tags-input.less';
|
||||
|
|
|
@ -8,9 +8,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-tree {
|
||||
.editor-sidebar {
|
||||
display: flex;
|
||||
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-small-mixin;
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
@outline-v-rhythm: 24px;
|
||||
@outline-h-rhythm: 24px;
|
||||
@outline-item-h-padding: @outline-h-rhythm * 0.25;
|
||||
|
||||
.outline-container {
|
||||
width: 100%;
|
||||
max-height: 50%; // While we don't support resizing.
|
||||
background-color: @file-tree-bg;
|
||||
}
|
||||
|
||||
|
@ -13,7 +8,7 @@
|
|||
flex-flow: column;
|
||||
height: 100%;
|
||||
font-size: @font-size-small;
|
||||
color: #fff;
|
||||
color: @file-tree-item-color;
|
||||
}
|
||||
|
||||
.outline-pane-disabled {
|
||||
|
@ -24,29 +19,33 @@
|
|||
.toolbar-small-mixin;
|
||||
.toolbar-alt-mixin;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: @outline-h-rhythm * 0.25;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid @toolbar-border-color;
|
||||
border-top: 1px solid @toolbar-border-color;
|
||||
}
|
||||
|
||||
.outline-header-expand-collapse-btn {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
padding: 0 (@outline-h-rhythm * 0.25) 0 0;
|
||||
font-size: inherit;
|
||||
vertical-align: inherit;
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
color: @file-tree-item-color;
|
||||
flex: 1 0 100%;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border-radius: @border-radius-base;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
&:hover,
|
||||
&:focus {
|
||||
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;
|
||||
font-family: @font-family-sans-serif;
|
||||
font-size: @font-size-small;
|
||||
color: #fff;
|
||||
color: @file-tree-item-color;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.outline-header-beta-badge {
|
||||
|
@ -87,7 +91,7 @@
|
|||
}
|
||||
|
||||
.outline-body-no-elements {
|
||||
color: @ol-blue-gray-2;
|
||||
color: @outline-no-items-color;
|
||||
text-align: center;
|
||||
padding: @outline-v-rhythm @outline-h-rhythm (@outline-v-rhythm * 2);
|
||||
margin-right: -(@outline-h-rhythm * 0.25);
|
||||
|
@ -95,11 +99,11 @@
|
|||
|
||||
.outline-body-link {
|
||||
display: block;
|
||||
color: #fff;
|
||||
color: @file-tree-item-color;
|
||||
text-decoration: underline;
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #fff;
|
||||
color: @file-tree-item-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +115,7 @@
|
|||
|
||||
&::before {
|
||||
content: '';
|
||||
background-color: @ol-blue-gray-3;
|
||||
background-color: @outline-line-guide-color;
|
||||
top: @outline-h-rhythm / 4;
|
||||
bottom: @outline-h-rhythm / 4;
|
||||
width: 1px;
|
||||
|
@ -127,9 +131,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.outline-item {
|
||||
}
|
||||
|
||||
.outline-item-no-children {
|
||||
padding-left: @outline-h-rhythm - @outline-item-h-padding;
|
||||
}
|
||||
|
@ -149,19 +150,19 @@
|
|||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: @file-tree-bg;
|
||||
color: @ol-blue-gray-2;
|
||||
color: @outline-expand-collapse-color;
|
||||
margin-right: -(@outline-item-h-padding);
|
||||
border-radius: @border-radius-base;
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
background-color: @ol-blue-gray-5;
|
||||
background-color: @file-tree-item-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.outline-item-link {
|
||||
display: inline;
|
||||
color: #fff;
|
||||
color: @file-tree-item-color;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
position: relative;
|
||||
|
@ -174,7 +175,7 @@
|
|||
&:hover,
|
||||
&:focus {
|
||||
outline: 0;
|
||||
background-color: @ol-blue-gray-5;
|
||||
background-color: @file-tree-item-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -57,6 +57,13 @@
|
|||
|
||||
@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-resizer-bg-color : @ol-blue-gray-1;
|
||||
@editor-resizer-bg-color-dragging: @ol-blue-gray-1;
|
||||
|
|
|
@ -1012,6 +1012,18 @@
|
|||
@file-tree-multiselect-hover-bg: @ol-dark-blue;
|
||||
@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-resizer-bg-color: @ol-blue-gray-5;
|
||||
@editor-resizer-bg-color-dragging: @ol-blue-gray-5;
|
||||
|
|
Loading…
Reference in a new issue