mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Add basic for pjax navigation
This commit is contained in:
@@ -1,13 +1,116 @@
|
||||
const ACTION_TYPES = require('./TYPES');
|
||||
const getPayload = require('../lib/getPayload');
|
||||
|
||||
const SUPPORTED = (
|
||||
typeof window !== 'undefined' &&
|
||||
window.history && window.history.pushState && window.history.replaceState &&
|
||||
// pushState isn't reliable on iOS until 5.
|
||||
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
|
||||
);
|
||||
|
||||
let PUSH_ID = 0;
|
||||
|
||||
/**
|
||||
* Generate a new state to be pushed or replaced
|
||||
* @param {Object}
|
||||
*/
|
||||
function genState() {
|
||||
return {
|
||||
id: (PUSH_ID++)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new url into the navigation
|
||||
* @param {String} uri
|
||||
* @return {Action} action
|
||||
*/
|
||||
function pushURI(uri) {
|
||||
return () => {
|
||||
const state = genState();
|
||||
|
||||
if (SUPPORTED) {
|
||||
window.history.pushState(state, '', uri);
|
||||
} else {
|
||||
redirect(uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current state in navigation
|
||||
* @param {String} uri
|
||||
* @return {Action} action
|
||||
*/
|
||||
function replaceURI(uri) {
|
||||
return () => {
|
||||
const state = genState();
|
||||
|
||||
if (SUPPORTED) {
|
||||
window.history.replaceState(state, '', uri);
|
||||
} else {
|
||||
redirect(uri);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard redirection
|
||||
* @param {String} uri
|
||||
* @return {Action} action
|
||||
*/
|
||||
function redirect(uri) {
|
||||
return () => {
|
||||
window.location.href = uri;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a new page and update the store accordingly
|
||||
* @param {String} uri
|
||||
* @param {Boolean} options.replace
|
||||
* @return {Action} action
|
||||
*/
|
||||
function fetchPage(uri) {
|
||||
return (dispatch, getState) => {
|
||||
function fetchPage(uri, options) {
|
||||
const { replace } = options;
|
||||
|
||||
return (dispatch, getState) => {
|
||||
const prevURI = location.href;
|
||||
dispatch({ type: ACTION_TYPES.PAGE_FETCH_START });
|
||||
|
||||
if (replace) {
|
||||
dispatch(replaceURI(uri));
|
||||
} else {
|
||||
dispatch(pushURI(uri));
|
||||
}
|
||||
|
||||
window.fetch(uri, {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
return response.text();
|
||||
}
|
||||
)
|
||||
.then(
|
||||
html => {
|
||||
const payload = getPayload(html);
|
||||
|
||||
if (!payload) {
|
||||
throw new Error('No payload found in page');
|
||||
}
|
||||
|
||||
dispatch({ type: ACTION_TYPES.PAGE_FETCH_END, payload });
|
||||
}
|
||||
)
|
||||
.catch(
|
||||
error => {
|
||||
dispatch(replaceURI(prevURI));
|
||||
dispatch(redirect(uri));
|
||||
|
||||
dispatch({ type: ACTION_TYPES.PAGE_FETCH_ERROR, error });
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +124,7 @@ function fetchArticle(article) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pushURI,
|
||||
fetchPage,
|
||||
fetchArticle
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const React = require('react');
|
||||
const ReactRedux = require('react-redux');
|
||||
const SummaryArticleShape = require('../shapes/SummaryArticle');
|
||||
|
||||
const Link = React.createClass({
|
||||
@@ -35,4 +36,4 @@ const Link = React.createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Link;
|
||||
module.exports = ReactRedux.connect()(Link);
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
const React = require('react');
|
||||
const ReactRedux = require('react-redux');
|
||||
const navigation = require('../actions/navigation');
|
||||
|
||||
/**
|
||||
* Check if an element is inside a link
|
||||
* @param {DOMElement} el
|
||||
* @param {String} name
|
||||
* @return {DOMElement|undefined
|
||||
*/
|
||||
function findParentByTagName(el, name) {
|
||||
while (el && el !== document) {
|
||||
if (el.tagName && el.tagName.toUpperCase() === name) {
|
||||
return el;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Return the `href` component of given URL object with the hash
|
||||
* portion removed.
|
||||
*
|
||||
* @param {Location|HTMLAnchorElement} location
|
||||
* @return {String}
|
||||
*/
|
||||
function stripHash(location) {
|
||||
return location.href.replace(/#.*/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a click event should be handled,
|
||||
* return the new url if it's a normal lcick
|
||||
*/
|
||||
function getHrefForEvent(event) {
|
||||
const link = findParentByTagName(event.target, 'A');
|
||||
|
||||
if (!link)
|
||||
return;
|
||||
|
||||
// Middle click, cmd click, and ctrl click should open
|
||||
// links in a new tab as normal.
|
||||
if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
||||
return;
|
||||
|
||||
// Ignore cross origin links
|
||||
if (location.protocol !== link.protocol || location.hostname !== link.hostname)
|
||||
return;
|
||||
|
||||
// Ignore case when a hash is being tacked on the current URL
|
||||
if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location))
|
||||
return;
|
||||
|
||||
// Ignore event with default prevented
|
||||
if (event.defaultPrevented)
|
||||
return;
|
||||
|
||||
// Explicitly ignored
|
||||
if (link.getAttribute('data-nopjax'))
|
||||
return;
|
||||
|
||||
return link.href;
|
||||
}
|
||||
|
||||
/*
|
||||
Wrapper to bind all navigation events to fetch pages.
|
||||
*/
|
||||
|
||||
const PJAXWrapper = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.node,
|
||||
dispatch: React.PropTypes.func
|
||||
},
|
||||
|
||||
onClick(event) {
|
||||
const { dispatch } = this.props;
|
||||
const href = getHrefForEvent(event);
|
||||
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
dispatch(navigation.fetchPage(href));
|
||||
|
||||
},
|
||||
|
||||
onPopState(event) {
|
||||
const { dispatch } = this.props;
|
||||
event.preventDefault();
|
||||
|
||||
dispatch(navigation.fetchPage(location.href, { replace: true }));
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.onClick, false);
|
||||
window.addEventListener('popstate', this.onPopState, false);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onClick, false);
|
||||
window.removeEventListener('popstate', this.onPopState, false);
|
||||
},
|
||||
|
||||
render() {
|
||||
return React.Children.only(this.props.children);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ReactRedux.connect()(PJAXWrapper);
|
||||
@@ -1,3 +1,5 @@
|
||||
const ACTION_TYPES = require('../actions/TYPES');
|
||||
|
||||
const composeReducer = require('../composeReducer');
|
||||
const createReducer = require('../createReducer');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { Record } = require('immutable');
|
||||
const ACTION_TYPES = require('../actions/TYPES');
|
||||
|
||||
const DEFAULTS = {
|
||||
title: '',
|
||||
@@ -16,5 +17,15 @@ class PageState extends Record(DEFAULTS) {
|
||||
}
|
||||
|
||||
module.exports = (state, action) => {
|
||||
return PageState.create(state);
|
||||
state = PageState.create(state);
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case ACTION_TYPES.PAGE_FETCH_END:
|
||||
return state.merge(action.payload.page);
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const React = require('react');
|
||||
const { Provider } = require('react-redux');
|
||||
const { InjectedComponent } = require('./components/InjectedComponent');
|
||||
const PJAXWrapper = require('./components/PJAXWrapper');
|
||||
|
||||
/**
|
||||
* Render the application for a store
|
||||
@@ -10,7 +11,9 @@ const { InjectedComponent } = require('./components/InjectedComponent');
|
||||
function renderWithStore(store) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<InjectedComponent matching={{ role: 'Body' }} />
|
||||
<PJAXWrapper>
|
||||
<InjectedComponent matching={{ role: 'Body' }} />
|
||||
</PJAXWrapper>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user