diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug
index 6e72a55aa2..88fa7ad4d5 100644
--- a/services/web/app/views/project/editor/file-tree.pug
+++ b/services/web/app/views/project/editor/file-tree.pug
@@ -1,102 +1,113 @@
-aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }", ng-show="ui.view != 'history' || !history.isV2").full-size
- .toolbar.toolbar-filetree(ng-if="permissions.write")
- a(
- href,
- ng-click="openNewDocModal()",
- tooltip-html="'"+translate('new_file').replace(' ', '
')+"'",
- tooltip-placement="bottom"
- )
- i.fa.fa-fw.fa-file
- a(
- href,
- ng-click="openNewFolderModal()",
- tooltip-html="'"+translate('new_folder').replace(' ', '
')+"'",
- tooltip-placement="bottom"
- )
- i.fa.fa-fw.fa-folder
- a(
- href,
- ng-click="openUploadFileModal()",
- tooltip=translate('upload'),
- tooltip-placement="bottom"
- )
- i.fa.fa-fw.fa-upload
-
- .toolbar-right
- a(
- href,
- ng-click="startRenamingSelected()",
- tooltip=translate('rename'),
- tooltip-placement="bottom",
- ng-show="multiSelectedCount == 0"
- )
- i.fa.fa-fw.fa-pencil
- a(
- href,
- ng-click="openDeleteModalForSelected()",
- tooltip=translate('delete'),
- tooltip-placement="bottom",
- tooltip-append-to-body="true"
- )
- i.fa.fa-fw.fa-trash-o
-
-
- .file-tree-inner(
- ng-if="rootFolder",
- ng-controller="FileTreeRootFolderController",
- ng-class="{ 'no-toolbar': !permissions.write }"
+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
)
- ul.list-unstyled.file-tree-list(
- droppable="permissions.write"
- accept=".entity-name"
- on-drop-callback="onDrop"
+ .toolbar.toolbar-filetree(ng-if="permissions.write")
+ a(
+ href,
+ ng-click="openNewDocModal()",
+ tooltip-html="'"+translate('new_file').replace(' ', '
')+"'",
+ tooltip-placement="bottom"
+ )
+ i.fa.fa-fw.fa-file
+ a(
+ href,
+ ng-click="openNewFolderModal()",
+ tooltip-html="'"+translate('new_folder').replace(' ', '
')+"'",
+ tooltip-placement="bottom"
+ )
+ i.fa.fa-fw.fa-folder
+ a(
+ href,
+ ng-click="openUploadFileModal()",
+ tooltip=translate('upload'),
+ tooltip-placement="bottom"
+ )
+ i.fa.fa-fw.fa-upload
+
+ .toolbar-right
+ a(
+ href,
+ ng-click="startRenamingSelected()",
+ tooltip=translate('rename'),
+ tooltip-placement="bottom",
+ ng-show="multiSelectedCount == 0"
+ )
+ i.fa.fa-fw.fa-pencil
+ a(
+ href,
+ ng-click="openDeleteModalForSelected()",
+ tooltip=translate('delete'),
+ tooltip-placement="bottom",
+ tooltip-append-to-body="true"
+ )
+ i.fa.fa-fw.fa-trash-o
+
+
+ .file-tree-inner(
+ ng-if="rootFolder",
+ ng-controller="FileTreeRootFolderController",
+ ng-class="{ 'no-toolbar': !permissions.write }"
)
- li(
- ng-show="ui.pdfLayout == 'flat' && (ui.view == 'editor' || ui.view == 'pdf' || ui.view == 'file')"
- ng-class="{ 'selected': ui.view == 'pdf' }"
- ng-controller="PdfViewToggleController"
+ ul.list-unstyled.file-tree-list(
+ droppable="permissions.write"
+ accept=".entity-name"
+ on-drop-callback="onDrop"
)
- .entity
- .entity-name(
- ng-click="togglePdfView()"
- )
- i.fa.fa-fw.toggle
- i.fa.fa-fw.fa-file-pdf-o
- | PDF
+ li(
+ ng-show="ui.pdfLayout == 'flat' && (ui.view == 'editor' || ui.view == 'pdf' || ui.view == 'file')"
+ ng-class="{ 'selected': ui.view == 'pdf' }"
+ ng-controller="PdfViewToggleController"
+ )
+ .entity
+ .entity-name(
+ ng-click="togglePdfView()"
+ )
+ i.fa.fa-fw.toggle
+ i.fa.fa-fw.fa-file-pdf-o
+ | PDF
- file-entity(
- entity="entity",
- permissions="permissions",
- ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
- )
+ file-entity(
+ entity="entity",
+ permissions="permissions",
+ ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
+ )
- li(ng-show="deletedDocs.length > 0 && ui.view == 'history'")
- h3 #{translate("deleted_files")}
- li(
- ng-class="{ 'selected': entity.selected }",
- ng-repeat="entity in deletedDocs | orderBy:'name'",
- ng-controller="FileTreeEntityController",
- ng-show="ui.view == 'history'"
- )
- .entity
- .entity-name(
- ng-click="select($event)"
- )
- //- Just a spacer to align with folders
- i.fa.fa-fw.toggle
- i.fa.fa-fw.fa-file
-
- span {{ entity.name }}
+ li(ng-show="deletedDocs.length > 0 && ui.view == 'history'")
+ h3 #{translate("deleted_files")}
+ li(
+ ng-class="{ 'selected': entity.selected }",
+ ng-repeat="entity in deletedDocs | orderBy:'name'",
+ ng-controller="FileTreeEntityController",
+ ng-show="ui.view == 'history'"
+ )
+ .entity
+ .entity-name(
+ ng-click="select($event)"
+ )
+ //- Just a spacer to align with folders
+ i.fa.fa-fw.toggle
+ i.fa.fa-fw.fa-file
+ 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"
)
diff --git a/services/web/app/views/project/editor/history/fileTreeV2.pug b/services/web/app/views/project/editor/history/fileTreeV2.pug
index b6f298d418..4deacd6489 100644
--- a/services/web/app/views/project/editor/history/fileTreeV2.pug
+++ b/services/web/app/views/project/editor/history/fileTreeV2.pug
@@ -1,4 +1,4 @@
-aside.file-tree.full-size(
+aside.editor-sidebar.full-size(
ng-controller="HistoryV2FileTreeController"
ng-if="ui.view == 'history' && history.isV2"
)
diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js
index e97fc70faf..ec25e7e5f1 100644
--- a/services/web/frontend/js/ide.js
+++ b/services/web/frontend/js/ide.js
@@ -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'
diff --git a/services/web/frontend/js/ide/directives/layout.js b/services/web/frontend/js/ide/directives/layout.js
index 594da8a69c..becf1c3583 100644
--- a/services/web/frontend/js/ide/directives/layout.js
+++ b/services/web/frontend/js/ide/directives/layout.js
@@ -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) {
diff --git a/services/web/frontend/js/ide/directives/verticalResizablePanes.js b/services/web/frontend/js/ide/directives/verticalResizablePanes.js
new file mode 100644
index 0000000000..1c9bc2decb
--- /dev/null
+++ b/services/web/frontend/js/ide/directives/verticalResizablePanes.js
@@ -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)
+ }
+}))
diff --git a/services/web/frontend/js/ide/editor/EditorManager.js b/services/web/frontend/js/ide/editor/EditorManager.js
index bfeec187c4..f05df497c8 100644
--- a/services/web/frontend/js/ide/editor/EditorManager.js
+++ b/services/web/frontend/js/ide/editor/EditorManager.js
@@ -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
diff --git a/services/web/frontend/js/ide/outline/OutlineManager.js b/services/web/frontend/js/ide/outline/OutlineManager.js
index 7fa2ba79ec..7a987be1fb 100644
--- a/services/web/frontend/js/ide/outline/OutlineManager.js
+++ b/services/web/frontend/js/ide/outline/OutlineManager.js
@@ -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()
diff --git a/services/web/frontend/js/ide/outline/components/OutlinePane.js b/services/web/frontend/js/ide/outline/components/OutlinePane.js
index 5ab2574e30..21da61d5cc 100644
--- a/services/web/frontend/js/ide/outline/components/OutlinePane.js
+++ b/services/web/frontend/js/ide/outline/components/OutlinePane.js
@@ -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 }) {
>