diff --git a/scripts/components/Directory.js b/scripts/components/Directory.js index 9d18f98..3006192 100644 --- a/scripts/components/Directory.js +++ b/scripts/components/Directory.js @@ -3,19 +3,21 @@ import ReactMixin from 'react-mixin'; import LocalStorageMixin from 'react-localstorage'; import request from 'superagent'; import FileNodes from './FileNodes'; +import { stopAutoScroll } from './scroll'; export default class Directory extends React.Component { - static get propTypes() { return { url: PropTypes.string, - name: PropTypes.string + name: PropTypes.string, + current: PropTypes.bool }; } static get getDefaultProps() { return { url: '', - name: '' + name: '', + current: false }; } @@ -27,11 +29,40 @@ export default class Directory extends React.Component { }; } + // also runs when the expanded flag is restored from localStorage + componentDidUpdate(prevProps, prevState) { + if (this.state.expanded && !prevState.expanded) { + request + .get(this.getExploreUrl()) + .end((err, res) => { + if (err || !this.state.expanded) { + return; + } + try { + this.setState({ children: JSON.parse(res.text) }); + } catch (e) { + // not JSON, e.g. a sign-in page after the session expired + } + }); + } + } + getLocalStorageKey() { return this.props.url; } - toggleFolder(path) { + // persist only the expanded flag; children are always loaded from the server + // eslint-disable-next-line class-methods-use-this + getStateFilterKeys() { + return ['expanded']; + } + + getExploreUrl() { + return this.props.url; + } + + toggleFolder() { + stopAutoScroll(); if (this.state.expanded) { this.setState({ expanded: false, @@ -39,26 +70,14 @@ export default class Directory extends React.Component { }); } else { this.setState({ expanded: true }); - request - .get(path) - .end((err, res) => { - if (err) { - return; - } - try { - this.setState({ children: JSON.parse(res.text) }); - } catch (e) { - return; - } - }); } } render() { const arrow = this.state.expanded ? 'octicon octicon-chevron-down' : 'octicon octicon-chevron-right'; return ( -
  • - - Files + Files
    diff --git a/scripts/components/scroll.js b/scripts/components/scroll.js new file mode 100644 index 0000000..b1af2f3 --- /dev/null +++ b/scripts/components/scroll.js @@ -0,0 +1,22 @@ +// Keeps the current file or folder visible while the tree restores its expanded folders +// after a page load. Stops once the user expands or collapses a folder, so the tree +// doesn't jump back to it. +let enabled = true; + +export function stopAutoScroll() { + enabled = false; +} + +// scroll only the tree (not the page) +export function scrollToCurrent(element) { + const tree = enabled && element && element.closest('.file-tree'); + const current = tree && tree.querySelector('li.current'); + if (!current) { + return; + } + const item = current.firstElementChild.getBoundingClientRect(); + const view = tree.getBoundingClientRect(); + if (item.top < view.top || item.bottom > view.bottom) { + tree.scrollTop += (item.top - view.top) - ((view.height - item.height) / 2); + } +} diff --git a/src/main/resources/explorer/assets/plugin-explorer.css b/src/main/resources/explorer/assets/plugin-explorer.css index 876b132..f48f264 100644 --- a/src/main/resources/explorer/assets/plugin-explorer.css +++ b/src/main/resources/explorer/assets/plugin-explorer.css @@ -78,3 +78,19 @@ li.active .file-tree { .file-node > a:hover { opacity: 0.6; } +/* the file or folder shown on the current page */ +.file-tree li.current > a, +.file-tree li.current > .folder-expander { + font-weight: bold; +} +.file-tree li.current > a { + background-color: #e8f1f8; +} +/* collapsed (mini) sidebar: show only the icon, like the core menu items */ +@media (min-width: 768px) { + .sidebar-mini.sidebar-collapse .tree-node > .root-expander, + .sidebar-mini.sidebar-collapse .tree-node > .file-tree, + .sidebar-mini.sidebar-collapse .submenu-files > span { + display: none; + } +} diff --git a/test/components/Directory.spec.js b/test/components/Directory.spec.js index 6723080..fa608a6 100644 --- a/test/components/Directory.spec.js +++ b/test/components/Directory.spec.js @@ -34,9 +34,11 @@ describe('Directory', () => { }); it('collapses without issuing a network call on second toggle', () => { - sinon.stub(request, 'get'); + sinon.stub(request, 'get').returns({ end: () => {} }); const wrapper = shallow(); + // expanding loads the children, like restoring the expanded flag from localStorage wrapper.setState({ expanded: true, children: [{ name: 'x', url: '/x', isDirectory: false }] }); + request.get.resetHistory(); wrapper.instance().toggleFolder('/owner/repo/explore/main/src'); wrapper.update();