Rename "navigation" to "history" and add loading bar

This commit is contained in:
Samy Pesse
2016-10-13 16:17:00 +02:00
parent c439cf8513
commit 05646996c0
16 changed files with 239 additions and 55 deletions
+10 -10
View File
@@ -1,16 +1,16 @@
module.exports = {
// Components
REGISTER_COMPONENT: 'components/register',
UNREGISTER_COMPONENT: 'components/unregister',
REGISTER_COMPONENT: 'components/register',
UNREGISTER_COMPONENT: 'components/unregister',
// Navigation
NAVIGATION_ACTIVATE: 'navigation/activate',
NAVIGATION_DEACTIVATE: 'navigation/deactivate',
NAVIGATION_LISTEN: 'navigation/listen',
NAVIGATION_UPDATE: 'navigation/update',
PAGE_FETCH_START: 'navigation/fetch:start',
PAGE_FETCH_END: 'navigation/fetch:end',
PAGE_FETCH_ERROR: 'navigation/fetch:error',
HISTORY_ACTIVATE: 'history/activate',
HISTORY_DEACTIVATE: 'history/deactivate',
HISTORY_LISTEN: 'history/listen',
HISTORY_UPDATE: 'history/update',
PAGE_FETCH_START: 'history/fetch:start',
PAGE_FETCH_END: 'history/fetch:end',
PAGE_FETCH_ERROR: 'history/fetch:error',
// i18n
I18N_REGISTER_LOCALE: 'i18n/register:locale'
I18N_REGISTER_LOCALE: 'i18n/register:locale'
};
@@ -10,16 +10,16 @@ const SUPPORTED = (
);
/**
* Initialize the navigation
* Initialize the history
*/
function activate() {
return (dispatch, getState) => {
dispatch({
type: ACTION_TYPES.NAVIGATION_ACTIVATE,
type: ACTION_TYPES.HISTORY_ACTIVATE,
listener: (location) => {
location = Location.fromNative(location);
const prevLocation = getState().navigation.location;
const prevLocation = getState().history.location;
// Fetch page if required
if (!prevLocation || location.pathname !== prevLocation.pathname) {
@@ -31,7 +31,7 @@ function activate() {
// Update the location
dispatch({
type: ACTION_TYPES.NAVIGATION_UPDATE,
type: ACTION_TYPES.HISTORY_UPDATE,
location
});
}
@@ -48,13 +48,13 @@ function activate() {
*/
function emit(to) {
return (dispatch, getState) => {
const { listeners, history } = getState().navigation;
const { listeners, client } = getState().history;
if (!history) {
if (!client) {
return;
}
const location = Location.fromNative(history.location);
const location = Location.fromNative(client.location);
to = to || listeners;
@@ -65,24 +65,24 @@ function emit(to) {
}
/**
* Cleanup the navigation
* Cleanup the history
*/
function deactivate() {
return { type: ACTION_TYPES.NAVIGATION_DEACTIVATE };
return { type: ACTION_TYPES.HISTORY_DEACTIVATE };
}
/**
* Push a new url into the navigation
* Push a new url into the history
* @param {String|Location} location
* @return {Action} action
*/
function push(location) {
return (dispatch, getState) => {
const { history } = getState().navigation;
const { client } = getState().history;
location = Location.fromNative(location);
if (SUPPORTED) {
history.push(location.toNative());
client.push(location.toNative());
} else {
redirect(location.toString());
}
@@ -90,17 +90,17 @@ function push(location) {
}
/**
* Replace current state in navigation
* Replace current state in history
* @param {String|Location} location
* @return {Action} action
*/
function replace(location) {
return (dispatch, getState) => {
const { history } = getState().navigation;
const { client } = getState().history;
location = Location.fromNative(location);
if (SUPPORTED) {
history.replace(location.toNative());
client.replace(location.toNative());
} else {
redirect(location.toString());
}
@@ -125,7 +125,7 @@ function redirect(uri) {
*/
function listen(listener) {
return (dispatch, getState) => {
dispatch({ type: ACTION_TYPES.NAVIGATION_LISTEN, listener });
dispatch({ type: ACTION_TYPES.HISTORY_LISTEN, listener });
// Trigger for existing listeners
dispatch(emit([ listener ]));
@@ -163,7 +163,6 @@ function fetchPage(pathname) {
.catch(
error => {
// dispatch(redirect(pathname));
console.error(error);
dispatch({ type: ACTION_TYPES.PAGE_FETCH_ERROR, error });
}
);
@@ -1,6 +1,6 @@
const React = require('react');
const ReactRedux = require('react-redux');
const Navigation = require('../actions/navigation');
const History = require('../actions/history');
/**
* Check if an element is inside a link
@@ -83,7 +83,7 @@ const PJAXWrapper = React.createClass({
}
event.preventDefault();
dispatch(Navigation.push(href));
dispatch(History.push(href));
},
componentDidMount() {
@@ -9,7 +9,7 @@ const composeReducer = require('./composeReducer');
const Components = require('../actions/components');
const I18n = require('../actions/i18n');
const Navigation = require('../actions/navigation');
const History = require('../actions/history');
const isBrowser = (typeof window !== 'undefined');
@@ -21,7 +21,7 @@ const isBrowser = (typeof window !== 'undefined');
const corePlugin = new Plugin({
reduce: coreReducers,
actions: {
Components, I18n, Navigation
Components, I18n, History
}
});
@@ -4,7 +4,7 @@ const { InjectedComponent } = require('../components/InjectedComponent');
const PJAXWrapper = require('../components/PJAXWrapper');
const I18nProvider = require('../components/I18nProvider');
const ContextProvider = require('../components/ContextProvider');
const Navigation = require('../actions/navigation');
const History = require('../actions/history');
const contextShape = require('../shapes/context');
const GitBookApplication = React.createClass({
@@ -15,12 +15,12 @@ const GitBookApplication = React.createClass({
componentDidMount() {
const { context } = this.props;
context.dispatch(Navigation.activate());
context.dispatch(History.activate());
},
componentWillUnmount() {
const { context } = this.props;
context.dispatch(Navigation.deactivate());
context.dispatch(History.deactivate());
},
render() {
@@ -4,7 +4,7 @@ const ACTION_TYPES = require('../actions/TYPES');
const isServerSide = (typeof window === 'undefined');
const NavigationState = Record({
const HistoryState = Record({
// Current location
location: null,
// Are we loading a new page
@@ -15,12 +15,12 @@ const NavigationState = Record({
listeners: List(),
// Function to call to stop listening
unlisten: null,
// History instance
history: null
// HistoryJS instance
client: null
});
function reduceNavigation(state, action) {
state = state || NavigationState();
function reduceHistory(state, action) {
state = state || HistoryState();
switch (action.type) {
case ACTION_TYPES.PAGE_FETCH_START:
@@ -39,33 +39,33 @@ function reduceNavigation(state, action) {
error: action.error
});
case ACTION_TYPES.NAVIGATION_ACTIVATE:
const history = isServerSide ? createMemoryHistory() : createBrowserHistory();
const unlisten = history.listen(action.listener);
case ACTION_TYPES.HISTORY_ACTIVATE:
const client = isServerSide ? createMemoryHistory() : createBrowserHistory();
const unlisten = client.listen(action.listener);
// We can't use .merge since it convert history to an immutable
const newState = state
.set('history', history)
.set('client', client)
.set('unlisten', unlisten);
return newState;
case ACTION_TYPES.NAVIGATION_DEACTIVATE:
case ACTION_TYPES.HISTORY_DEACTIVATE:
if (state.unlisten) {
state.unlisten();
}
return state.merge({
history: null,
client: null,
unlisten: null
});
case ACTION_TYPES.NAVIGATION_UPDATE:
case ACTION_TYPES.HISTORY_UPDATE:
return state.merge({
location: action.location
});
case ACTION_TYPES.NAVIGATION_LISTEN:
case ACTION_TYPES.HISTORY_LISTEN:
return state.merge({
listeners: state.listeners.push(action.listener)
});
@@ -76,4 +76,4 @@ function reduceNavigation(state, action) {
}
}
module.exports = reduceNavigation;
module.exports = reduceHistory;
+1 -1
View File
@@ -3,7 +3,7 @@ const createReducer = require('../lib/createReducer');
module.exports = composeReducer(
createReducer('components', require('./components')),
createReducer('navigation', require('./navigation')),
createReducer('history', require('./history')),
createReducer('i18n', require('./i18n')),
// GitBook JSON
createReducer('config', require('./config')),
@@ -0,0 +1,11 @@
const React = require('react');
const locationShape = require('./Location');
const {
bool,
shape
} = React.PropTypes;
module.exports = shape({
loading: bool,
location: locationShape
});
@@ -0,0 +1,12 @@
const React = require('react');
const { map } = require('react-immutable-proptypes');
const {
string,
shape
} = React.PropTypes;
module.exports = shape({
pathname: string,
hash: string,
query: map
});
@@ -8,6 +8,8 @@ module.exports = {
Context: require('./Context'),
Page: require('./Page'),
File: require('./File'),
History: require('./History'),
Location: require('./Location'),
Readme: require('./Readme'),
Summary: require('./Summary'),
SummaryPart: require('./SummaryPart'),
+2 -2
View File
@@ -18,9 +18,9 @@ const onLocationChange = (location, dispatch) => {
};
module.exports = GitBook.createPlugin({
activate: (dispatch, getState, { Navigation, Components }) => {
activate: (dispatch, getState, { History, Components }) => {
// Register the navigation handler
dispatch(Navigation.listen(onLocationChange));
dispatch(History.listen(onLocationChange));
// Register components
dispatch(Components.registerComponent(SearchInput, { role: 'search:input' }));
@@ -0,0 +1,30 @@
.LoadingBar {
pointer-events: none;
transition: 400ms linear all;
.LoadingBar-Bar {
background: @color-primary;
height: 2px;
position: fixed;
top: 0;
left: 0;
z-index: 10000;
display: none;
width: 100%;
border-radius: 0 1px 1px 0;
transition: width 350ms;
}
.LoadingBar-Shadow {
content: '';
position: absolute;
top: 0;
right: 0;
width: 70px;
height: 2px;
border-radius: 50%;
opacity: .45;
box-shadow: @color-primary 1px 0 6px 1px;
}
}
@@ -14,6 +14,7 @@
@import "Search.less";
@import "Body.less";
@import "Dropdown.less";
@import "LoadingBar.less";
* {
.box-sizing(border-box);
@@ -1,3 +1,5 @@
// Colors
@color-primary: hsl(207, 100%, 50%); // rgb(44, 106, 254);
// Fonts
@font-family-serif: Georgia, serif;
@font-family-sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -16,7 +18,7 @@
@summary-article-padding-v: 10px;
@summary-article-padding-h: 15px;
@summary-article-color: hsl(207, 15%, 25%);
@summary-article-hover-color: hsl(207, 100%, 50%);
@summary-article-hover-color: @color-primary;
@summary-article-active-color: @summary-article-color;
@summary-article-active-background: #f5f5f5;
// Page
@@ -0,0 +1,124 @@
const GitBook = require('gitbook-core');
const { React } = GitBook;
/**
* Displays a progress bar (YouTube-like) at the top of container
* Based on https://github.com/lonelyclick/react-loading-bar/blob/master/src/Loading.jsx
*/
const LoadingBar = React.createClass({
propTypes: {
show: React.PropTypes.bool
},
getDefaultProps() {
return {
show: false
};
},
getInitialState() {
return {
size: 0,
disappearDelayHide: false, // when dispappear, first transition then display none
percent: 0,
appearDelayWidth: 0 // when appear, first display block then transition width
};
},
componentWillReceiveProps(nextProps) {
const { show } = nextProps;
if (show) {
this.show();
} else {
this.hide();
}
},
shouldComponentUpdate(nextProps, nextState) {
return true; // !shallowEqual(nextState, this.state)
},
show() {
let { size, percent } = this.state;
const appearDelayWidth = size === 0;
percent = calculatePercent(percent);
this.setState({
size: ++size,
appearDelayWidth,
percent
});
if (appearDelayWidth) {
setTimeout(() => {
this.setState({
appearDelayWidth: false
});
});
}
},
hide() {
let { size } = this.state;
if (--size < 0) {
this.setState({ size: 0 });
return;
}
this.setState({
size: 0,
disappearDelayHide: true,
percent: 1
});
setTimeout(() => {
this.setState({
disappearDelayHide: false,
percent: 0
});
}, 500);
},
getBarStyle() {
const { disappearDelayHide, appearDelayWidth, percent } = this.state;
return {
width: appearDelayWidth ? 0 : percent * 100 + '%',
display: disappearDelayHide || percent > 0 ? 'block' : 'none'
};
},
getShadowStyle() {
const { percent, disappearDelayHide } = this.state;
return {
display: disappearDelayHide || percent > 0 ? 'block' : 'none'
};
},
render() {
return (
<div className="LoadingBar">
<div className="LoadingBar-Bar" style={this.getBarStyle()}>
<div className="LoadingBar-Shadow"
style={this.getShadowStyle()}>
</div>
</div>
</div>
);
}
});
function calculatePercent(percent) {
percent = percent || 0;
// How much of remaining bar we advance
const progress = 0.1 + Math.random() * 0.3;
return percent + progress * (1 - percent);
}
module.exports = LoadingBar;
@@ -3,6 +3,7 @@ const { React } = GitBook;
const Sidebar = require('./Sidebar');
const Body = require('./Body');
const LoadingBar = require('./LoadingBar');
const Theme = React.createClass({
propTypes: {
@@ -10,16 +11,18 @@ const Theme = React.createClass({
page: GitBook.Shapes.Page,
summary: GitBook.Shapes.Summary,
readme: GitBook.Shapes.Readme,
history: GitBook.Shapes.History,
sidebar: React.PropTypes.object,
// Other props
children: React.PropTypes.node
},
render() {
const { page, summary, children, sidebar, readme } = this.props;
const { page, summary, children, sidebar, readme, history } = this.props;
return (
<GitBook.FlexLayout column className="GitBook book">
<LoadingBar show={history.loading} />
<GitBook.Head
title={page.title}
titleTemplate="%s - GitBook" />
@@ -43,6 +46,6 @@ const Theme = React.createClass({
}
});
module.exports = GitBook.connect(Theme, ({page, summary, sidebar, readme}) => {
return { page, summary, sidebar, readme };
module.exports = GitBook.connect(Theme, ({page, summary, sidebar, readme, history}) => {
return { page, summary, sidebar, readme, history };
});