diff options
Diffstat (limited to 'packages/gitbook-plugin-theme-default/src/components')
7 files changed, 511 insertions, 0 deletions
diff --git a/packages/gitbook-plugin-theme-default/src/components/Body.js b/packages/gitbook-plugin-theme-default/src/components/Body.js new file mode 100644 index 0000000..c61a2f3 --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/Body.js @@ -0,0 +1,121 @@ +const debounce = require('debounce'); +const GitBook = require('gitbook-core'); +const { React } = GitBook; + +const Page = require('./Page'); +const Toolbar = require('./Toolbar'); + +const HEADINGS_SELECTOR = 'h1[id],h2[id],h3[id],h4[id]'; + +/** + * Get offset of an element relative to a parent container. + * @param {DOMElement} container + * @param {DOMElement} element + * @return {Number} offset + */ +function getOffset(container, element, type = 'Top') { + const parent = element.parentElement; + let base = 0; + + if (parent != container) { + base = getOffset(container, parent, type); + } + + return base + element[`offset${type}`]; +} + +/** + * Find the current heading anchor for a scroll position. + * @param {DOMElement} container + * @param {Number} top + * @return {String} + */ +function getHeadingID(container, top) { + let id; + const headings = container.querySelectorAll(HEADINGS_SELECTOR); + + headings.forEach(heading => { + if (id) { + return; + } + + const offset = getOffset(container, heading); + + if (offset > top) { + id = heading.getAttribute('id'); + } + }); + + return id; +} + +const Body = React.createClass({ + propTypes: { + page: GitBook.PropTypes.Page, + readme: GitBook.PropTypes.Readme, + history: GitBook.PropTypes.History, + updateURI: React.PropTypes.func + }, + + getInitialState() { + this.debouncedOnScroll = debounce(this.onScroll, 300); + return {}; + }, + + /** + * User is scrolling the page, update the location with current section's ID. + */ + onScroll() { + const { scrollContainer } = this; + const { history, updateURI } = this.props; + const { location } = history; + + // Find the id matching the current scroll position + const hash = getHeadingID(scrollContainer, scrollContainer.scrollTop); + + // Update url if changed + if (hash !== location.hash) { + updateURI(location.merge({ hash })); + } + }, + + /** + * Component has been updated with a new location, + * scroll to the right anchor. + */ + componentDidUpdate() { + + }, + + render() { + const { page, readme } = this.props; + + return ( + <GitBook.InjectedComponent matching={{ role: 'body:wrapper' }}> + <div + className="Body page-wrapper" + onScroll={this.debouncedOnScroll} + ref={div => this.scrollContainer = div} + > + <GitBook.InjectedComponent matching={{ role: 'toolbar:wrapper' }}> + <Toolbar title={page.title} readme={readme} /> + </GitBook.InjectedComponent> + <GitBook.InjectedComponent matching={{ role: 'page:wrapper' }}> + <Page page={page} /> + </GitBook.InjectedComponent> + </div> + </GitBook.InjectedComponent> + ); + } +}); + +module.exports = GitBook.connect(Body, + () => { + return {}; + }, + ({ History }, dispatch) => { + return { + updateURI: (location) => dispatch(History.replace(location)) + }; + } +); diff --git a/packages/gitbook-plugin-theme-default/src/components/LoadingBar.js b/packages/gitbook-plugin-theme-default/src/components/LoadingBar.js new file mode 100644 index 0000000..11e1ddb --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/LoadingBar.js @@ -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; diff --git a/packages/gitbook-plugin-theme-default/src/components/Page.js b/packages/gitbook-plugin-theme-default/src/components/Page.js new file mode 100644 index 0000000..cbce704 --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/Page.js @@ -0,0 +1,30 @@ +const GitBook = require('gitbook-core'); +const { React } = GitBook; + +const Page = React.createClass({ + propTypes: { + page: GitBook.PropTypes.Page + }, + + render() { + const { page } = this.props; + + return ( + <div className="PageContainer"> + <GitBook.InjectedComponent matching={{ role: 'search:container:results' }} props={this.props}> + <div className="Page"> + <GitBook.InjectedComponentSet matching={{ role: 'page:header' }} props={this.props} /> + + <GitBook.InjectedComponent matching={{ role: 'page:container' }} props={this.props}> + <GitBook.HTMLContent html={page.content} /> + </GitBook.InjectedComponent> + + <GitBook.InjectedComponentSet matching={{ role: 'page:footer' }} props={this.props} /> + </div> + </GitBook.InjectedComponent> + </div> + ); + } +}); + +module.exports = Page; diff --git a/packages/gitbook-plugin-theme-default/src/components/Sidebar.js b/packages/gitbook-plugin-theme-default/src/components/Sidebar.js new file mode 100644 index 0000000..ab628df --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/Sidebar.js @@ -0,0 +1,25 @@ +const GitBook = require('gitbook-core'); +const { React } = GitBook; + +const Summary = require('./Summary'); + +const Sidebar = React.createClass({ + propTypes: { + summary: GitBook.PropTypes.Summary + }, + + render() { + const { summary } = this.props; + + return ( + <div className="Sidebar-Flex"> + <div className="Sidebar book-summary"> + <GitBook.InjectedComponent matching={{ role: 'search:container:input' }} /> + <Summary summary={summary} /> + </div> + </div> + ); + } +}); + +module.exports = Sidebar; diff --git a/packages/gitbook-plugin-theme-default/src/components/Summary.js b/packages/gitbook-plugin-theme-default/src/components/Summary.js new file mode 100644 index 0000000..ef6ab3f --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/Summary.js @@ -0,0 +1,111 @@ +const classNames = require('classnames'); +const GitBook = require('gitbook-core'); +const { React } = GitBook; + +let SummaryArticle = React.createClass({ + propTypes: { + active: React.PropTypes.bool, + article: GitBook.PropTypes.SummaryArticle + }, + + render() { + const { article, active } = this.props; + const className = classNames('SummaryArticle', { + active + }); + + return ( + <GitBook.InjectedComponent matching={{ role: 'summary:article' }} props={this.props}> + <li className={className}> + {article.ref ? + <GitBook.Link to={article}>{article.title}</GitBook.Link> + : <span>{article.title}</span>} + </li> + </GitBook.InjectedComponent> + ); + } +}); +SummaryArticle = GitBook.connect(SummaryArticle, ({page}, {article}) => { + return { + active: page.level === article.level + }; +}); + +const SummaryArticles = React.createClass({ + propTypes: { + articles: GitBook.PropTypes.listOf(GitBook.PropTypes.SummaryArticle) + }, + + render() { + const { articles } = this.props; + + return ( + <GitBook.InjectedComponent matching={{ role: 'summary:articles' }} props={this.props}> + <ul className="SummaryArticles"> + {articles.map(article => <SummaryArticle key={article.level} article={article} />)} + </ul> + </GitBook.InjectedComponent> + ); + } +}); + +const SummaryPart = React.createClass({ + propTypes: { + part: GitBook.PropTypes.SummaryPart + }, + + render() { + const { part } = this.props; + const { title, articles } = part; + + const titleEL = title ? <h2 className="SummaryPart-Title">{title}</h2> : null; + + return ( + <GitBook.InjectedComponent matching={{ role: 'summary:part' }} props={this.props}> + <div className="SummaryPart"> + {titleEL} + <SummaryArticles articles={articles} /> + </div> + </GitBook.InjectedComponent> + ); + } +}); + +const SummaryParts = React.createClass({ + propTypes: { + parts: GitBook.PropTypes.listOf(GitBook.PropTypes.SummaryPart) + }, + + render() { + const { parts } = this.props; + + return ( + <GitBook.InjectedComponent matching={{ role: 'summary:parts' }} props={this.props}> + <div className="SummaryParts"> + {parts.map((part, i) => <SummaryPart key={i} part={part} />)} + </div> + </GitBook.InjectedComponent> + ); + } +}); + +const Summary = React.createClass({ + propTypes: { + summary: GitBook.PropTypes.Summary + }, + + render() { + const { summary } = this.props; + const { parts } = summary; + + return ( + <GitBook.InjectedComponent matching={{ role: 'summary:container' }} props={this.props}> + <div className="Summary book-summary"> + <SummaryParts parts={parts} /> + </div> + </GitBook.InjectedComponent> + ); + } +}); + +module.exports = Summary; diff --git a/packages/gitbook-plugin-theme-default/src/components/Theme.js b/packages/gitbook-plugin-theme-default/src/components/Theme.js new file mode 100644 index 0000000..b323fc4 --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/Theme.js @@ -0,0 +1,57 @@ +const GitBook = require('gitbook-core'); +const { React, ReactCSSTransitionGroup } = GitBook; + +const Sidebar = require('./Sidebar'); +const Body = require('./Body'); +const LoadingBar = require('./LoadingBar'); + +const Theme = React.createClass({ + propTypes: { + // State + page: GitBook.PropTypes.Page, + summary: GitBook.PropTypes.Summary, + readme: GitBook.PropTypes.Readme, + history: GitBook.PropTypes.History, + sidebar: React.PropTypes.object, + // Other props + children: React.PropTypes.node + }, + + render() { + 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" /> + <GitBook.ImportCSS href="gitbook/theme-default/theme.css" /> + + <GitBook.FlexBox> + <ReactCSSTransitionGroup + component={GitBook.FlexLayout} + transitionName="Layout" + transitionEnterTimeout={300} + transitionLeaveTimeout={300}> + {sidebar.open ? ( + <Sidebar key={0} summary={summary} /> + ) : null} + <div key={1} className="Body-Flex"> + <Body + page={page} + readme={readme} + history={history} + /> + </div> + </ReactCSSTransitionGroup> + </GitBook.FlexBox> + {children} + </GitBook.FlexLayout> + ); + } +}); + +module.exports = GitBook.connect(Theme, ({page, summary, sidebar, readme, history}) => { + return { page, summary, sidebar, readme, history }; +}); diff --git a/packages/gitbook-plugin-theme-default/src/components/Toolbar.js b/packages/gitbook-plugin-theme-default/src/components/Toolbar.js new file mode 100644 index 0000000..d426a40 --- /dev/null +++ b/packages/gitbook-plugin-theme-default/src/components/Toolbar.js @@ -0,0 +1,43 @@ +const GitBook = require('gitbook-core'); +const { React } = GitBook; + +const sidebar = require('../actions/sidebar'); + +const Toolbar = React.createClass({ + propTypes: { + title: React.PropTypes.string.isRequired, + dispatch: React.PropTypes.func, + readme: GitBook.PropTypes.Readme + }, + + onToggle() { + const { dispatch } = this.props; + dispatch(sidebar.toggle()); + }, + + render() { + const { title, readme } = this.props; + + return ( + <GitBook.FlexLayout className="Toolbar"> + <GitBook.FlexBox className="Toolbar-left"> + <GitBook.InjectedComponentSet align="flex-end" matching={{ role: 'toolbar:buttons:left' }}> + <GitBook.Button onClick={this.onToggle}> + <GitBook.Icon id="align-justify" /> + </GitBook.Button> + </GitBook.InjectedComponentSet> + </GitBook.FlexBox> + <GitBook.FlexBox auto> + <h1 className="Toolbar-Title"> + <GitBook.Link to={readme.file}>{title}</GitBook.Link> + </h1> + </GitBook.FlexBox> + <GitBook.FlexBox className="Toolbar-right"> + <GitBook.InjectedComponentSet align="flex-end" matching={{ role: 'toolbar:buttons:right' }} /> + </GitBook.FlexBox> + </GitBook.FlexLayout> + ); + } +}); + +module.exports = GitBook.connect(Toolbar); |