Files
docbox/src/components/app.js
2016-03-17 14:02:58 -04:00

224 lines
7.8 KiB
JavaScript

import React from 'react';
import Navigation from './navigation';
import Content from './content';
import RoundedToggle from './rounded_toggle';
import PureRenderMixin from 'react-pure-render/mixin';
import GithubSlugger from 'github-slugger';
import debounce from 'lodash.debounce';
import { brandNames, brandClasses } from '../../custom';
import qs from 'querystring';
let slugger = new GithubSlugger();
let slug = title => { slugger.reset(); return slugger.slug(title); };
let languageOptions = ['cURL', 'CLI', 'Python', 'JavaScript'];
let defaultLanguage = 'cURL';
let debouncedReplaceState = debounce(hash => {
window.history.replaceState('', '', hash);
}, 100);
var App = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
content: React.PropTypes.string.isRequired,
ast: React.PropTypes.object.isRequired
},
getInitialState() {
var active = 'Introduction';
if (process.browser) {
let hash = window.location.hash.split('#').pop();
let languageFromURL = qs.parse(window.location.search.substring(1)).language;
let language = languageOptions.includes(languageFromURL) ?
languageFromURL : defaultLanguage;
let mqls = {
desktop: window.matchMedia('(min-width: 961px)'),
tablet: window.matchMedia('(max-width: 960px)'),
mobile: window.matchMedia('(max-width: 640px)')
};
Object.keys(mqls).forEach(key => {
mqls[key].addListener(this.mediaQueryChanged);
});
if (hash) {
let headingForHash = this.props.ast.children
.filter(child => child.type === 'heading')
.find(heading => heading.data.id === hash);
if (headingForHash) {
active = headingForHash.children[0].value;
}
}
return {
// media queryMatches
mqls: mqls,
// object of currently matched queries, like { desktop: true }
queryMatches: {},
language: language,
columnMode: 2,
activeSection: active,
// for the toggle-able navigation on mobile
showNav: false
};
} else {
return {
mqls: {
desktop: true
},
queryMatches: {
desktop: true
},
language: defaultLanguage,
activeSection: '',
showNav: false
};
}
},
toggleNav() {
this.setState({ showNav: !this.state.showNav });
},
componentDidMount() {
this.mediaQueryChanged();
this.onScroll = debounce(this._onScroll, 100);
document.addEventListener('scroll', this.onScroll);
this._onScroll();
},
_onScroll() {
var sections = document.querySelectorAll('div.section');
if (!sections.length) return;
for (var i = 0; i < sections.length; i++) {
var rect = sections[i].getBoundingClientRect();
if (rect.bottom > 0) {
this.setState({
activeSection: sections[i].getAttribute('data-title')
});
return;
}
}
},
mediaQueryChanged() {
this.setState({
queryMatches: {
mobile: this.state.mqls.mobile.matches,
tablet: this.state.mqls.tablet.matches,
desktop: this.state.mqls.desktop.matches
}
});
},
componentWillUnmount() {
Object.keys(this.state.mqls).forEach(key =>
this.state.mqls[key].removeListener(this.mediaQueryChanged));
document.body.removeEventListener('scroll', this.onScroll);
},
onChangeLanguage(language) {
this.setState({ language }, () => {
if (window.history) {
window.history.pushState(null, null,
`?${qs.stringify({ language })}${window.location.hash}`);
}
});
},
componentDidUpdate(_, prevState) {
if (prevState.activeSection !== this.state.activeSection) {
// when the section changes, replace the hash
debouncedReplaceState(`#${slug(this.state.activeSection)}`);
} else if (prevState.language !== this.state.language ||
prevState.columnMode !== this.state.columnMode) {
// when the language changes, use the hash to set scroll
window.location.hash = window.location.hash;
}
},
navigationItemClicked(activeSection) {
setTimeout(() => {
this.setState({ activeSection });
}, 10);
if (!this.state.queryMatches.desktop) {
this.toggleNav();
}
},
toggleColumnMode() {
this.setState({
columnMode: this.state.columnMode === 1 ? 2 : 1
});
},
render() {
let { ast } = this.props;
let { activeSection, queryMatches, showNav, columnMode } = this.state;
let col1 = columnMode === 1 && queryMatches.desktop;
return (<div className='container unlimiter'>
{/* Content background */ }
{(!col1 && !queryMatches.mobile) && <div className={`fixed-top fixed-right ${queryMatches.desktop && 'space-left16'}`}>
<div className='fill-light col6 pin-right'></div>
</div>}
{/* Desktop nav */ }
{queryMatches.desktop && <div className='space-top5 scroll-styled pad1 width16 sidebar fixed-left fill-dark dark'>
<Navigation
navigationItemClicked={this.navigationItemClicked}
activeSection={activeSection}
ast={ast} />
</div>}
{/* Content */ }
<div className={`${queryMatches.desktop && 'space-left16'}`}>
<div className={col1 ? 'col8 margin1' : ''}>
<Content
leftClassname={col1 ? 'space-bottom4 pad2x prose clip' : 'space-bottom8 col6 pad2x prose clip'}
rightClassname={col1 ? 'space-bottom2 pad2 prose clip fill-light space-top5' : 'space-bottom4 col6 pad2 prose clip fill-light space-top5'}
ast={ast}
language={this.state.language.toLowerCase()}/>
</div>
</div>
{/* Language toggle */ }
<div className={`fixed-top ${queryMatches.desktop && 'space-left16'}`}>
<div className={`events fill-light bottom-shadow pad1 ${col1 ? '' : 'col6 pin-topright'} ${queryMatches.tablet ? 'dark fill-blue' : ''} ${queryMatches.mobile ? 'space-top5 fixed-topright' : ''}`}>
<div className='space-right1 small quiet inline'>
Show examples in:
</div>
<RoundedToggle
options={languageOptions}
onChange={this.onChangeLanguage}
active={this.state.language} />
<div className='fr pad0'>
{queryMatches.desktop ?
<a
title={`Display as ${col1 ? 2 : 1} column`}
onClick={this.toggleColumnMode}
style={{ cursor: 'pointer' }}
className={`icon quiet caret-${col1 ? 'right' : 'left'} pad0 fill-darken0 round`}></a> : null}
</div>
</div>
</div>
{/* Header */ }
<div className={`fill-dark dark bottom-shadow fixed-top ${queryMatches.tablet ? 'pad1y pad2x col6' : 'pad0 width16'}`}>
<a href='/' className={`active space-top1 space-left1 pin-topleft icon round dark pad0 ${brandClasses}`}></a>
<div className={`strong small pad0
${queryMatches.mobile ? 'space-left3' : ''}
${queryMatches.tablet ? 'space-left2' : 'space-left4 line-height15' }`}>
{queryMatches.desktop ? brandNames.desktop :
queryMatches.mobile ? brandNames.mobile : brandNames.tablet}
</div>
{queryMatches.tablet && <div>
<button
onClick={this.toggleNav}
className={`short quiet pin-topright button rcon ${showNav ? 'caret-up' : 'caret-down'} space-right1 space-top1`}>
<span className='micro'>{activeSection}</span>
</button>
{showNav && <div
className='fixed-left keyline-top fill-dark pin-left col6 pad2 scroll-styled space-top5'>
<Navigation
navigationItemClicked={this.navigationItemClicked}
activeSection={activeSection}
ast={ast} />
</div>}
</div>}
</div>
</div>);
}
});
module.exports = App;