189 lines
4.9 KiB
JavaScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { Link } from '@storybook/router';
import { SyntaxHighlighter } from '@storybook/components';
import createElement from 'react-syntax-highlighter/create-element';
import { EVENT_ID } from './events';
const StyledStoryLink = styled(Link)(({ theme }) => ({
display: 'block',
textDecoration: 'none',
borderRadius: theme.appBorderRadius,
color: 'inherit',
'&:hover': {
background: theme.background.hoverable,
},
}));
const SelectedStoryHighlight = styled.div(({ theme }) => ({
background: theme.background.hoverable,
borderRadius: theme.appBorderRadius,
}));
const StyledSyntaxHighlighter = styled(SyntaxHighlighter)(({ theme }) => ({
fontSize: theme.typography.size.s2 - 1,
}));
const areLocationsEqual = (a, b) =>
a.startLoc.line === b.startLoc.line &&
a.startLoc.col === b.startLoc.col &&
a.endLoc.line === b.endLoc.line &&
a.endLoc.col === b.endLoc.col;
const getLocationKeys = locationsMap =>
locationsMap
? Array.from(Object.keys(locationsMap)).sort(
(key1, key2) => locationsMap[key1].startLoc.line - locationsMap[key2].startLoc.line
)
: [];
export default class StoryPanel extends Component {
state = { source: 'loading source...' };
componentDidMount() {
this.mounted = true;
const { api } = this.props;
api.on(EVENT_ID, this.listener);
}
componentDidUpdate() {
if (this.selectedStoryRef) {
this.selectedStoryRef.scrollIntoView();
}
}
componentWillUnmount() {
const { api } = this.props;
api.off(EVENT_ID, this.listener);
}
setSelectedStoryRef = ref => {
this.selectedStoryRef = ref;
};
listener = ({ edition: { source }, location: { currentLocation, locationsMap } }) => {
const locationsKeys = getLocationKeys(locationsMap);
this.setState({
source,
currentLocation,
locationsMap,
locationsKeys,
});
};
createPart = (rows, stylesheet, useInlineStyles) =>
rows.map((node, i) =>
createElement({
node,
stylesheet,
useInlineStyles,
key: `code-segement${i}`,
})
);
createStoryPart = (rows, stylesheet, useInlineStyles, location, id) => {
const { currentLocation } = this.state;
const first = location.startLoc.line - 1;
const last = location.endLoc.line;
const storyRows = rows.slice(first, last);
const story = this.createPart(storyRows, stylesheet, useInlineStyles);
const storyKey = `${first}-${last}`;
if (location && currentLocation && areLocationsEqual(location, currentLocation)) {
return (
<SelectedStoryHighlight key={storyKey} ref={this.setSelectedStoryRef}>
{story}
</SelectedStoryHighlight>
);
}
return (
<StyledStoryLink to={`/story/${id}`} key={storyKey}>
{story}
</StyledStoryLink>
);
};
createParts = (rows, stylesheet, useInlineStyles) => {
const { locationsMap, locationsKeys } = this.state;
const parts = [];
let lastRow = 0;
locationsKeys.forEach(key => {
const location = locationsMap[key];
const first = location.startLoc.line - 1;
const last = location.endLoc.line;
const start = this.createPart(rows.slice(lastRow, first), stylesheet, useInlineStyles);
const storyPart = this.createStoryPart(rows, stylesheet, useInlineStyles, location, key);
parts.push(start);
parts.push(storyPart);
lastRow = last;
});
const lastPart = this.createPart(rows.slice(lastRow), stylesheet, useInlineStyles);
parts.push(lastPart);
return parts;
};
lineRenderer = ({ rows, stylesheet, useInlineStyles }) => {
const { locationsMap, locationsKeys } = this.state;
// because of the usage of lineRenderer, all lines will be wrapped in a span
// these spans will recieve all classes on them for some reason
// which makes colours casecade incorrectly
// this removed that list of classnames
const myrows = rows.map(({ properties, ...rest }) => ({
...rest,
properties: { className: [] },
}));
if (!locationsMap || !locationsKeys.length) {
return this.createPart(myrows, stylesheet, useInlineStyles);
}
const parts = this.createParts(myrows, stylesheet, useInlineStyles);
return <span>{parts}</span>;
};
render() {
const { active } = this.props;
const { source } = this.state;
return active ? (
<StyledSyntaxHighlighter
language="jsx"
showLineNumbers="true"
renderer={this.lineRenderer}
format={false}
copyable={false}
padded
>
{source}
</StyledSyntaxHighlighter>
) : null;
}
}
StoryPanel.propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({
selectStory: PropTypes.func.isRequired,
emit: PropTypes.func,
on: PropTypes.func,
off: PropTypes.func,
}).isRequired,
};