Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions scripts/components/Directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}

Expand All @@ -27,38 +29,55 @@ 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,
children: []
});
} 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 (
<li className="folder-node">
<button className="folder-expander" onClick={() => this.toggleFolder(this.props.url)}>
<li className={this.props.current ? 'folder-node current' : 'folder-node'}>
<button className="folder-expander" onClick={() => this.toggleFolder()}>
<i className={arrow} />
<i className="menu-icon octicon octicon-file-directory" />
{this.props.name}
Expand Down
9 changes: 7 additions & 2 deletions scripts/components/File.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React, { PropTypes } from 'react';

export default class File extends React.Component {

static get propTypes() {
return {
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
current: PropTypes.bool,
};
}
static get defaultProps() {
return {
current: false,
};
}

render() {
return (
<li className="file-node">
<li className={this.props.current ? 'file-node current' : 'file-node'}>
<a href={this.props.url}>
<i className="menu-icon octicon octicon-file" />
{this.props.name}
Expand Down
49 changes: 41 additions & 8 deletions scripts/components/FileNodes.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
import React, { PropTypes } from 'react';
import File from './File';
import Directory from './Directory';
import { scrollToCurrent } from './scroll';

export default class FileNodes extends React.Component {
// compare decoded paths without trailing slash,
// the browser and the server may encode them differently
function normalizePath(path) {
let decoded = path;
try {
decoded = decodeURIComponent(path);
} catch (e) {
// keep it as it is
}
return decoded.replace(/\/+$/, '');
}

export default class FileNodes extends React.Component {
static get propTypes() {
return {
data: PropTypes.array.isRequired,
data: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
isDirectory: PropTypes.bool.isRequired,
})).isRequired,
};
}

componentDidMount() {
scrollToCurrent(this.list);
}

componentDidUpdate(prevProps) {
if (prevProps.data !== this.props.data) {
scrollToCurrent(this.list);
}
}

render() {
const nodes = this.props.data.map(node => (
node.isDirectory ?
<Directory key={node.url} name={node.name} url={node.url.replace('/tree/', '/explore/')} />
: <File key={node.url} name={node.name} url={node.url} />
));
const currentPath = normalizePath(document.location.pathname);
const nodes = this.props.data.map((node) => {
const current = normalizePath(node.url) === currentPath;
return node.isDirectory ?
<Directory
key={node.url}
name={node.name}
url={node.url.replace('/tree/', '/explore/')}
current={current}
/>
: <File key={node.url} name={node.name} url={node.url} current={current} />;
});
return (
<ul>
<ul ref={(el) => { this.list = el; }}>
{nodes}
</ul>
);
Expand Down
9 changes: 6 additions & 3 deletions scripts/components/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import FileNodes from './FileNodes';
import Directory from './Directory';

export default class Root extends Directory {

componentWillMount() {
const logo = document.querySelector('header.main-header a.logo');
if (!logo) {
Expand Down Expand Up @@ -34,16 +33,20 @@ export default class Root extends Directory {
return `${this.state.rootPath}/${this.state.branch}`;
}

getExploreUrl() {
return `${this.state.rootPath}/explore/${this.state.branch}`;
}

render() {
const arrow = this.state.expanded ? 'octicon octicon-chevron-down' : 'octicon octicon-chevron-right';
return (
<div className="tree-node">
<button className="root-expander btn btn-default" onClick={() => this.toggleFolder(`${this.state.rootPath}/explore/${this.state.branch}`)} >
<button className="root-expander btn btn-default" onClick={() => this.toggleFolder()} >
<i className={arrow} />
</button>
<a href={this.state.rootPath} className="submenu-files" >
<i className="menu-icon octicon octicon-file-directory" />
Files
<span>Files</span>
</a>
<div className="file-tree" style={this.state.expanded ? {} : { display: 'none' }} >
<FileNodes data={this.state.children} />
Expand Down
22 changes: 22 additions & 0 deletions scripts/components/scroll.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 16 additions & 0 deletions src/main/resources/explorer/assets/plugin-explorer.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
4 changes: 3 additions & 1 deletion test/components/Directory.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Directory name="src" url="/owner/repo/explore/main/src" />);
// 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();
Expand Down
Loading