Files
2019-08-15 00:54:44 +02:00

276 lines
9.5 KiB
JavaScript
Executable File

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom';
import _ from 'lodash';
import { LayoutContent } from './LayoutContent';
import { LayoutNavbar } from './LayoutNavbar';
import { LayoutSidebar } from './LayoutSidebar';
import { PageConfigContext } from './PageConfigContext';
import { ThemeClass } from './../Theme';
import config from './../../../config';
const findChildByType = (children, targetType) => {
let result;
React.Children.forEach(children, (child) => {
if (child.type.layoutPartName === targetType.layoutPartName) {
result = child;
}
});
return result;
};
const findChildrenByType = (children, targetType) => {
return _.filter(React.Children.toArray(children), (child) =>
child.type.layoutPartName === targetType.layoutPartName);
};
const responsiveBreakpoints = {
'xs': { max: 575.8 },
'sm': { min: 576, max: 767.8 },
'md': { min: 768, max: 991.8 },
'lg': { min: 992, max: 1199.8 },
'xl': { min: 1200 }
};
class Layout extends React.Component {
static propTypes = {
children: PropTypes.node,
sidebarSlim: PropTypes.bool,
location: PropTypes.object,
favIcons: PropTypes.array
}
constructor(props) {
super(props);
this.state = {
sidebarHidden: false,
navbarHidden: false,
footerHidden: false,
sidebarCollapsed: false,
screenSize: '',
animationsDisabled: true,
pageTitle: null,
pageDescription: config.siteDescription,
pageKeywords: config.siteKeywords
};
this.lastLgSidebarCollapsed = false;
this.containerRef = React.createRef();
}
componentDidMount() {
// Determine the current window size
// and set it up in the context state
const layoutAdjuster = () => {
const { screenSize } = this.state;
let currentScreenSize;
_.forOwn(responsiveBreakpoints, (value, key) => {
const queryParts = [
`${ _.isUndefined(value.min) ? '' : `(min-width: ${value.min}px)` }`,
`${ _.isUndefined(value.max) ? '' : `(max-width: ${value.max}px)`}`
];
const query = _.compact(queryParts).join(' and ');
if (window.matchMedia(query).matches) {
currentScreenSize = key;
}
});
if (screenSize !== currentScreenSize) {
this.setState({ screenSize: currentScreenSize });
this.updateLayoutOnScreenSize(currentScreenSize);
}
};
// Add window initialization
if (typeof window !== 'undefined') {
window.addEventListener('resize', () => {
setTimeout(layoutAdjuster.bind(this), 0);
});
layoutAdjuster();
window.requestAnimationFrame(() => {
this.setState({ animationsDisabled: false });
});
}
// Add document initialization
if (typeof document !== 'undefined') {
this.bodyElement = document.body;
this.documentElement = document.documentElement;
}
}
componentDidUpdate(prevProps, prevState) {
// Prevent content scrolling in overlay mode
if (
this.bodyElement && this.documentElement && (
this.state.screenSize === 'xs' ||
this.state.screenSize === 'sm' ||
this.state.screenSize === 'md'
)
) {
if (prevState.sidebarCollapsed !== this.state.sidebarCollapsed) {
// Most of the devices
const styleUpdate = this.state.sidebarCollapsed ? {
overflowY: 'auto',
touchAction: 'auto'
}: {
overflowY: 'hidden',
touchAction: 'none'
}
Object.assign(this.bodyElement.style, styleUpdate);
Object.assign(this.documentElement.style, styleUpdate);
}
}
// After location change
if (prevProps.location.pathname !== this.props.location.pathname) {
// Scroll to top
if (this.bodyElement && this.documentElement) {
this.documentElement.scrollTop = this.bodyElement.scrollTop = 0;
}
// Hide the sidebar when in overlay mode
if (
!this.state.sidebarCollapsed && (
this.state.screenSize === 'xs' ||
this.state.screenSize === 'sm' ||
this.state.screenSize === 'md'
)
) {
// Add some time to prevent jank while the dom is updating
setTimeout(() => {
this.setState({ sidebarCollapsed: true });
}, 100);
}
}
// Update positions of STICKY navbars
this.updateNavbarsPositions();
}
updateLayoutOnScreenSize(screenSize) {
if (
screenSize === 'md' ||
screenSize === 'sm' ||
screenSize === 'xs'
) {
// Save for recovering to lg later
this.lastLgSidebarCollapsed = this.state.sidebarCollapsed;
this.setState({ sidebarCollapsed: true });
} else {
this.setState({ sidebarCollapsed: this.lastLgSidebarCollapsed });
}
}
updateNavbarsPositions() {
// eslint-disable-next-line react/no-find-dom-node
const containerElement = ReactDOM.findDOMNode(this.containerRef.current);
if (containerElement) {
const navbarElements = containerElement.querySelectorAll(":scope .layout__navbar");
// Calculate and update style.top of each navbar
let totalNavbarsHeight = 0;
navbarElements.forEach((navbarElement) => {
const navbarBox = navbarElement.getBoundingClientRect();
navbarElement.style.top = `${totalNavbarsHeight}px`;
totalNavbarsHeight += navbarBox.height;
});
}
}
toggleSidebar() {
this.setState({
sidebarCollapsed: !this.state.sidebarCollapsed
});
}
setElementsVisibility(elements) {
this.setState(_.pick(elements, ['sidebarHidden', 'navbarHidden', 'footerHidden']));
}
render() {
const { children, favIcons } = this.props;
const sidebar = findChildByType(children, LayoutSidebar);
const navbars = findChildrenByType(children, LayoutNavbar);
const content = findChildByType(children, LayoutContent);
const otherChildren = _.differenceBy(
React.Children.toArray(children),
[
sidebar,
...navbars,
content
],
'type'
);
const layoutClass = classNames('layout', 'layout--animations-enabled', {
//'layout--only-navbar': this.state.sidebarHidden && !this.state.navbarHidden
});
return (
<PageConfigContext.Provider
value={{
...this.state,
sidebarSlim: !!this.props.sidebarSlim && (
this.state.screenSize === 'lg' ||
this.state.screenSize === 'xl'
),
toggleSidebar: this.toggleSidebar.bind(this),
setElementsVisibility: this.setElementsVisibility.bind(this),
changeMeta: (metaData) => { this.setState(metaData) }
}}
>
<Helmet>
<meta charSet="utf-8" />
<title>{ config.siteTitle + (this.state.pageTitle ? ` - ${this.state.pageTitle}` : '') }</title>
<link rel="canonical" href={ config.siteCannonicalUrl } />
<meta name="description" content={ this.state.pageDescription } />
{
_.map(favIcons, (favIcon, index) => (
<link { ...favIcon } key={ index } />
))
}
</Helmet>
<ThemeClass>
{(themeClass) => (
<div className={ classNames(layoutClass, themeClass) } ref={ this.containerRef }>
{
!this.state.sidebarHidden && sidebar && React.cloneElement(sidebar, {
sidebarSlim: !!this.props.sidebarSlim && this.state.sidebarCollapsed && (
this.state.screenSize === 'lg' || this.state.screenSize === 'xl'
),
sidebarCollapsed: !this.props.sidebarSlim && this.state.sidebarCollapsed
})
}
<div className="layout__wrap">
{ !this.state.navbarHidden && navbars }
{ content }
</div>
{ otherChildren }
</div>
)}
</ThemeClass>
</PageConfigContext.Provider>
);
}
}
const routedLayout = withRouter(Layout);
export { routedLayout as Layout };