2019-05-22 11:05:01 +02:00

463 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint no-underscore-dangle: 0 */
import React, { Component, createElement } from 'react';
import { isForwardRef } from 'react-is';
import { polyfill } from 'react-lifecycles-compat';
import PropTypes from 'prop-types';
import global from 'global';
import marksy from 'marksy';
import Node from './Node';
import { Pre } from './markdown';
import { getDisplayName, getType } from '../react-utils';
global.STORYBOOK_REACT_CLASSES = global.STORYBOOK_REACT_CLASSES || [];
const { STORYBOOK_REACT_CLASSES } = global;
const stylesheetBase = {
button: {
base: {
fontFamily: 'sans-serif',
fontSize: '12px',
display: 'block',
position: 'fixed',
border: 'none',
background: '#027ac5',
color: '#fff',
padding: '5px 15px',
cursor: 'pointer',
},
topRight: {
top: 0,
right: 0,
borderRadius: '0 0 0 5px',
},
},
info: {
position: 'fixed',
background: 'white',
top: 0,
bottom: 0,
left: 0,
right: 0,
padding: '0 40px',
overflow: 'auto',
zIndex: 99999,
},
children: {
position: 'relative',
zIndex: 0,
},
infoBody: {
fontFamily: 'Helvetica Neue, Helvetica, Segoe UI, Arial, freesans, sans-serif',
color: 'black',
fontWeight: 300,
lineHeight: 1.45,
fontSize: '15px',
border: '1px solid #eee',
padding: '20px 40px 40px',
borderRadius: '2px',
backgroundColor: '#fff',
marginTop: '20px',
marginBottom: '20px',
},
infoContent: {
marginBottom: 0,
},
infoStory: {},
jsxInfoContent: {
borderTop: '1px solid #eee',
margin: '20px 0 0 0',
},
header: {
h1: {
margin: 0,
padding: 0,
fontSize: '35px',
},
h2: {
margin: '0 0 10px 0',
padding: 0,
fontWeight: 400,
fontSize: '22px',
},
h3: {
margin: '0 0 10px 0',
padding: 0,
fontWeight: 400,
fontSize: '18px',
},
body: {
borderBottom: '1px solid #eee',
paddingTop: 10,
marginBottom: 10,
},
},
source: {
h1: {
margin: '20px 0 0 0',
padding: '0 0 5px 0',
fontSize: '25px',
borderBottom: '1px solid #EEE',
},
},
propTableHead: {
margin: '20px 0 0 0',
},
};
class Story extends Component {
constructor(props, ...args) {
super(props, ...args);
this.state = {
open: false,
};
this.marksy = marksy({
createElement,
elements: props.components,
});
}
_renderStory() {
const { stylesheet } = this.state;
const { children } = this.props;
return (
<div id="story-root" style={stylesheet.infoStory}>
{children}
</div>
);
}
_renderInline() {
const { stylesheet } = this.state;
return (
<div>
{this._renderInlineHeader()}
{this._renderStory()}
<div style={stylesheet.infoPage}>
<div style={stylesheet.infoBody}>
{this._getInfoContent()}
{this._getComponentDescription()}
{this._getSourceCode()}
{this._getPropTables()}
</div>
</div>
</div>
);
}
_renderInlineHeader() {
const { stylesheet } = this.state;
const infoHeader = this._getInfoHeader();
return (
infoHeader && (
<div style={stylesheet.infoPage}>
<div style={stylesheet.infoBody}>{infoHeader}</div>
</div>
)
);
}
_renderOverlay() {
const { stylesheet, open } = this.state;
const { children } = this.props;
const buttonStyle = {
...stylesheet.button.base,
...stylesheet.button.topRight,
};
const infoStyle = Object.assign({}, stylesheet.info);
if (!open) {
infoStyle.display = 'none';
}
const openOverlay = () => {
this.setState({ open: true });
return false;
};
const closeOverlay = () => {
this.setState({ open: false });
return false;
};
return (
<div>
<div style={stylesheet.children}>{children}</div>
<button
type="button"
style={buttonStyle}
onClick={openOverlay}
className="info__show-button"
>
Show Info
</button>
<div style={infoStyle} className="info__overlay">
<button
type="button"
style={buttonStyle}
onClick={closeOverlay}
className="info__close-button"
>
×
</button>
<div style={stylesheet.infoPage}>
<div style={stylesheet.infoBody}>
{this._getInfoHeader()}
{this._getInfoContent()}
{this._getComponentDescription()}
{this._getSourceCode()}
{this._getPropTables()}
</div>
</div>
</div>
</div>
);
}
_getInfoHeader() {
const { stylesheet } = this.state;
const { context, showHeader } = this.props;
if (!context || !showHeader) {
return null;
}
return (
<div style={stylesheet.header.body}>
<h1 style={stylesheet.header.h1}>{context.kind}</h1>
<h2 style={stylesheet.header.h2}>{context.name}</h2>
</div>
);
}
_getInfoContent() {
const { info, showInline } = this.props;
const { stylesheet } = this.state;
if (!info) {
return '';
}
if (React.isValidElement(info)) {
return (
<div style={showInline ? stylesheet.jsxInfoContent : stylesheet.infoContent}>{info}</div>
);
}
const lines = info.split('\n');
while (lines[0].trim() === '') {
lines.shift();
}
let padding = 0;
const matches = lines[0].match(/^ */);
if (matches) {
padding = matches[0].length;
}
const source = lines.map(s => s.slice(padding)).join('\n');
return <div style={stylesheet.infoContent}>{this.marksy(source).tree}</div>;
}
_getComponentDescription() {
const {
context: { kind, name },
} = this.props;
let retDiv = null;
const validMatches = [kind, name];
if (Object.keys(STORYBOOK_REACT_CLASSES).length) {
Object.keys(STORYBOOK_REACT_CLASSES).forEach(key => {
if (validMatches.includes(STORYBOOK_REACT_CLASSES[key].name)) {
const componentDescription = STORYBOOK_REACT_CLASSES[key].docgenInfo.description;
retDiv = <div>{this.marksy(componentDescription).tree}</div>;
}
});
}
return retDiv;
}
_getSourceCode() {
const {
showSource,
maxPropsIntoLine,
maxPropObjectKeys,
maxPropArrayLength,
maxPropStringLength,
children,
} = this.props;
const { stylesheet } = this.state;
if (!showSource) {
return null;
}
return (
<div>
<h1 style={stylesheet.source.h1}>Story Source</h1>
<Pre>
{React.Children.map(children, (root, idx) => (
<Node
key={idx} // eslint-disable-line react/no-array-index-key
node={root}
depth={0}
maxPropsIntoLine={maxPropsIntoLine}
maxPropObjectKeys={maxPropObjectKeys}
maxPropArrayLength={maxPropArrayLength}
maxPropStringLength={maxPropStringLength}
/>
))}
</Pre>
</div>
);
}
_getPropTables() {
const {
children,
propTablesExclude,
maxPropObjectKeys,
maxPropArrayLength,
maxPropStringLength,
excludedPropTypes,
} = this.props;
let { propTables } = this.props;
const { stylesheet } = this.state;
const types = new Map();
if (propTables === null) {
return null;
}
if (!children) {
return null;
}
if (propTables) {
propTables.forEach(type => {
types.set(type, true);
});
}
// depth-first traverse and collect types
const extract = innerChildren => {
if (!innerChildren) {
return;
}
if (Array.isArray(innerChildren)) {
innerChildren.forEach(extract);
return;
}
if (innerChildren.props && innerChildren.props.children) {
extract(innerChildren.props.children);
}
if (isForwardRef(innerChildren)) {
try {
// this might fail because of hooks being used
extract(innerChildren.type.render(innerChildren.props));
} catch (e) {
// do nothing
}
}
if (
typeof innerChildren === 'string' ||
typeof innerChildren.type === 'string' ||
(Array.isArray(propTablesExclude) && // also ignore excluded types
~propTablesExclude.indexOf(innerChildren.type)) // eslint-disable-line no-bitwise
) {
return;
}
if (innerChildren.type && !types.has(innerChildren.type)) {
types.set(innerChildren.type, true);
}
};
// extract components from children
extract(children);
const array = Array.from(types.keys());
array.sort((a, b) => (getDisplayName(a) > getDisplayName(b) ? 1 : -1));
propTables = array.map((type, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={`${getDisplayName(type)}_${i}`}>
<h3 style={stylesheet.propTableHead}>"{getDisplayName(type)}" Component</h3>
<this.props.PropTable
type={getType(type)}
maxPropObjectKeys={maxPropObjectKeys}
maxPropArrayLength={maxPropArrayLength}
maxPropStringLength={maxPropStringLength}
excludedPropTypes={excludedPropTypes}
/>
</div>
));
if (!propTables || propTables.length === 0) {
return null;
}
return (
<div>
<h1 style={stylesheet.source.h1}>Prop Types</h1>
{propTables}
</div>
);
}
render() {
const { showInline } = this.props;
// <ThemeProvider theme={stylesheet}></ThemeProvider>
return showInline ? this._renderInline() : this._renderOverlay();
}
}
Story.getDerivedStateFromProps = ({ styles }) => ({ stylesheet: styles(stylesheetBase) });
Story.displayName = 'Story';
Story.propTypes = {
context: PropTypes.shape({
kind: PropTypes.string,
name: PropTypes.string,
}),
info: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
propTables: PropTypes.arrayOf(PropTypes.func),
propTablesExclude: PropTypes.arrayOf(PropTypes.func),
showInline: PropTypes.bool,
showHeader: PropTypes.bool,
showSource: PropTypes.bool,
// eslint-disable-next-line react/no-unused-prop-types
styles: PropTypes.func.isRequired,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
components: PropTypes.shape({}),
maxPropsIntoLine: PropTypes.number.isRequired,
maxPropObjectKeys: PropTypes.number.isRequired,
maxPropArrayLength: PropTypes.number.isRequired,
maxPropStringLength: PropTypes.number.isRequired,
excludedPropTypes: PropTypes.arrayOf(PropTypes.string),
};
Story.defaultProps = {
context: null,
info: '',
children: null,
propTables: null,
propTablesExclude: [],
showInline: false,
showHeader: true,
showSource: true,
components: {},
excludedPropTypes: [],
};
polyfill(Story);
export default Story;