Merge branch 'master' of github.com:jorilallo/atlas into tom/static
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Editor, Plain } from 'slate';
|
||||
import keydown from 'react-keydown';
|
||||
import type { Document, State, Editor as EditorType } from './types';
|
||||
import type { State, Editor as EditorType } from './types';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Flex from 'components/Flex';
|
||||
import ClickablePadding from './components/ClickablePadding';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import BlockInsert from './components/BlockInsert';
|
||||
import Placeholder from './components/Placeholder';
|
||||
import Contents from './components/Contents';
|
||||
import Markdown from './serializer';
|
||||
import createSchema from './schema';
|
||||
import createPlugins from './plugins';
|
||||
@@ -37,10 +39,7 @@ type KeyData = {
|
||||
editor: EditorType;
|
||||
schema: Object;
|
||||
plugins: Array<Object>;
|
||||
|
||||
state: {
|
||||
state: State,
|
||||
};
|
||||
@observable editorState: State;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -51,10 +50,10 @@ type KeyData = {
|
||||
onImageUploadStop: props.onImageUploadStop,
|
||||
});
|
||||
|
||||
if (props.text) {
|
||||
this.state = { state: Markdown.deserialize(props.text) };
|
||||
if (props.text.trim().length) {
|
||||
this.editorState = Markdown.deserialize(props.text);
|
||||
} else {
|
||||
this.state = { state: Plain.deserialize('') };
|
||||
this.editorState = Plain.deserialize('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,15 +73,16 @@ type KeyData = {
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (state: State) => {
|
||||
this.setState({ state });
|
||||
};
|
||||
onChange = (editorState: State) => {
|
||||
if (this.editorState !== editorState) {
|
||||
this.props.onChange(Markdown.serialize(editorState));
|
||||
}
|
||||
|
||||
onDocumentChange = (document: Document, state: State) => {
|
||||
this.props.onChange(Markdown.serialize(state));
|
||||
this.editorState = editorState;
|
||||
};
|
||||
|
||||
handleDrop = async (ev: SyntheticEvent) => {
|
||||
if (this.props.readOnly) return;
|
||||
// check if this event was already handled by the Editor
|
||||
if (ev.isDefaultPrevented()) return;
|
||||
|
||||
@@ -92,7 +92,9 @@ type KeyData = {
|
||||
|
||||
const files = getDataTransferFiles(ev);
|
||||
for (const file of files) {
|
||||
await this.insertImageFile(file);
|
||||
if (file.type.startsWith('image/')) {
|
||||
await this.insertImageFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,7 +164,7 @@ type KeyData = {
|
||||
const transform = state.transform();
|
||||
transform.collapseToStartOf(state.document);
|
||||
transform.focus();
|
||||
this.setState({ state: transform.apply() });
|
||||
this.editorState = transform.apply();
|
||||
};
|
||||
|
||||
focusAtEnd = () => {
|
||||
@@ -170,7 +172,7 @@ type KeyData = {
|
||||
const transform = state.transform();
|
||||
transform.collapseToEndOf(state.document);
|
||||
transform.focus();
|
||||
this.setState({ state: transform.apply() });
|
||||
this.editorState = transform.apply();
|
||||
};
|
||||
|
||||
render = () => {
|
||||
@@ -187,11 +189,12 @@ type KeyData = {
|
||||
>
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||
<Contents state={this.editorState} />
|
||||
{!readOnly &&
|
||||
<Toolbar state={this.state.state} onChange={this.onChange} />}
|
||||
<Toolbar state={this.editorState} onChange={this.onChange} />}
|
||||
{!readOnly &&
|
||||
<BlockInsert
|
||||
state={this.state.state}
|
||||
state={this.editorState}
|
||||
onChange={this.onChange}
|
||||
onInsertImage={this.insertImageFile}
|
||||
/>}
|
||||
@@ -202,10 +205,9 @@ type KeyData = {
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
emoji={emoji}
|
||||
state={this.state.state}
|
||||
state={this.editorState}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onDocumentChange={this.onDocumentChange}
|
||||
onSave={onSave}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
@@ -246,22 +248,6 @@ const StyledEditor = styled(Editor)`
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: #dedede;
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
|
||||
@@ -6,11 +6,13 @@ import { color } from 'styles/constants';
|
||||
import type { Props } from '../types';
|
||||
|
||||
export default function Code({ children, node, readOnly, attributes }: Props) {
|
||||
const language = node.data.get('language') || 'javascript';
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{readOnly && <CopyButton text={node.text} />}
|
||||
<Pre>
|
||||
<code {...attributes}>
|
||||
<Pre className={`language-${language}`}>
|
||||
<code {...attributes} className={`language-${language}`}>
|
||||
{children}
|
||||
</code>
|
||||
</Pre>
|
||||
@@ -20,7 +22,7 @@ export default function Code({ children, node, readOnly, attributes }: Props) {
|
||||
|
||||
const Pre = styled.pre`
|
||||
padding: .5em 1em;
|
||||
background: ${color.smoke};
|
||||
background: ${color.smokeLight};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.smokeDark};
|
||||
|
||||
|
||||
149
frontend/components/Editor/components/Contents.js
Normal file
149
frontend/components/Editor/components/Contents.js
Normal file
@@ -0,0 +1,149 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { List } from 'immutable';
|
||||
import { color } from 'styles/constants';
|
||||
import headingToSlug from '../headingToSlug';
|
||||
import type { State, Block } from '../types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
state: State,
|
||||
};
|
||||
|
||||
@observer class Contents extends Component {
|
||||
props: Props;
|
||||
@observable activeHeading: ?string;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('scroll', this.updateActiveHeading);
|
||||
this.updateActiveHeading();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('scroll', this.updateActiveHeading);
|
||||
}
|
||||
|
||||
updateActiveHeading = () => {
|
||||
const elements = this.headingElements;
|
||||
if (!elements.length) return;
|
||||
|
||||
let activeHeading = elements[0].id;
|
||||
|
||||
for (const element of elements) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
if (bounds.top <= 0) activeHeading = element.id;
|
||||
}
|
||||
|
||||
this.activeHeading = activeHeading;
|
||||
};
|
||||
|
||||
get headingElements(): HTMLElement[] {
|
||||
const elements = [];
|
||||
const tagNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
for (const tagName of tagNames) {
|
||||
for (const ele of document.getElementsByTagName(tagName)) {
|
||||
elements.push(ele);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
get headings(): List<Block> {
|
||||
const { state } = this.props;
|
||||
|
||||
return state.document.nodes.filter((node: Block) => {
|
||||
if (!node.text) return false;
|
||||
return node.type.match(/^heading/);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
// If there are one or less headings in the document no need for a minimap
|
||||
if (this.headings.size <= 1) return null;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Sections>
|
||||
{this.headings.map(heading => {
|
||||
const slug = headingToSlug(heading);
|
||||
const active = this.activeHeading === slug;
|
||||
|
||||
return (
|
||||
<ListItem type={heading.type} active={active}>
|
||||
<Anchor href={`#${slug}`} active={active}>
|
||||
{heading.text}
|
||||
</Anchor>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</Sections>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 150px;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||
font-weight: ${props => (props.active ? 500 : 400)};
|
||||
opacity: 0;
|
||||
transition: all 100ms ease-in-out;
|
||||
margin-right: -5px;
|
||||
padding: 2px 0;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
color: ${color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled.li`
|
||||
position: relative;
|
||||
margin-left: ${props => (props.type.match(/heading[12]/) ? '8px' : '16px')};
|
||||
text-align: right;
|
||||
color: ${color.slate};
|
||||
padding-right: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:after {
|
||||
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||
content: "${props => (props.type.match(/heading[12]/) ? '—' : '–')}";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Sections = styled.ol`
|
||||
margin: 0 0 0 -8px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 13px;
|
||||
width: 100px;
|
||||
transition-delay: 1s;
|
||||
transition: width 100ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
width: 300px;
|
||||
transition-delay: 0s;
|
||||
|
||||
${Anchor} {
|
||||
opacity: 1;
|
||||
margin-right: 0;
|
||||
background: ${color.white};
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Contents;
|
||||
@@ -2,13 +2,12 @@
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import slug from 'slug';
|
||||
import headingToSlug from '../headingToSlug';
|
||||
import type { Node, Editor } from '../types';
|
||||
import Placeholder from './Placeholder';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<any>,
|
||||
children: React$Element<*>,
|
||||
placeholder?: boolean,
|
||||
parent: Node,
|
||||
node: Node,
|
||||
@@ -31,7 +30,7 @@ function Heading(props: Props) {
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
||||
const slugish = headingToSlug(node);
|
||||
const showHash = readOnly && !!slugish;
|
||||
const Component = component;
|
||||
const emoji = editor.props.emoji || '';
|
||||
@@ -40,8 +39,10 @@ function Heading(props: Props) {
|
||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||
|
||||
return (
|
||||
<Component {...rest}>
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
||||
<Component {...rest} id={slugish}>
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
|
||||
{children}
|
||||
</Wrapper>
|
||||
{showPlaceholder &&
|
||||
<Placeholder contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
@@ -53,7 +54,7 @@ function Heading(props: Props) {
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: inline;
|
||||
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
||||
margin-left: ${(props: Props) => (props.hasEmoji ? '-1.2em' : 0)}
|
||||
`;
|
||||
|
||||
const Anchor = styled.a`
|
||||
@@ -66,19 +67,31 @@ const Anchor = styled.a`
|
||||
}
|
||||
`;
|
||||
|
||||
export const Heading1 = styled(Heading)`
|
||||
export const StyledHeading = styled(Heading)`
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
${Anchor} {
|
||||
visibility: visible;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const Heading2 = Heading1.withComponent('h2');
|
||||
export const Heading3 = Heading1.withComponent('h3');
|
||||
export const Heading4 = Heading1.withComponent('h4');
|
||||
export const Heading5 = Heading1.withComponent('h5');
|
||||
export const Heading6 = Heading1.withComponent('h6');
|
||||
|
||||
export default Heading;
|
||||
export const Heading1 = (props: Props) => (
|
||||
<StyledHeading component="h1" {...props} />
|
||||
);
|
||||
export const Heading2 = (props: Props) => (
|
||||
<StyledHeading component="h2" {...props} />
|
||||
);
|
||||
export const Heading3 = (props: Props) => (
|
||||
<StyledHeading component="h3" {...props} />
|
||||
);
|
||||
export const Heading4 = (props: Props) => (
|
||||
<StyledHeading component="h4" {...props} />
|
||||
);
|
||||
export const Heading5 = (props: Props) => (
|
||||
<StyledHeading component="h5" {...props} />
|
||||
);
|
||||
export const Heading6 = (props: Props) => (
|
||||
<StyledHeading component="h6" {...props} />
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { fontWeight, color } from 'styles/constants';
|
||||
import Document from 'models/Document';
|
||||
import GoToIcon from 'components/Icon/GoToIcon';
|
||||
import NextIcon from 'components/Icon/NextIcon';
|
||||
|
||||
type Props = {
|
||||
innerRef?: Function,
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
function DocumentResult({ document, ...rest }: Props) {
|
||||
return (
|
||||
<ListItem {...rest} href="">
|
||||
<i><GoToIcon light /></i>
|
||||
<i><NextIcon light /></i>
|
||||
{document.title}
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
@@ -114,9 +114,10 @@ class LinkToolbar extends Component {
|
||||
const { state } = this.props;
|
||||
const transform = state.transform();
|
||||
|
||||
if (state.selection.isExpanded) {
|
||||
if (href) {
|
||||
transform.setInline({ type: 'link', data: { href } });
|
||||
} else {
|
||||
transform.unwrapInline('link');
|
||||
if (href) transform.wrapInline({ type: 'link', data: { href } });
|
||||
}
|
||||
|
||||
this.props.onChange(transform.apply());
|
||||
@@ -179,7 +180,7 @@ class LinkToolbar extends Component {
|
||||
}
|
||||
|
||||
const SearchResults = styled.div`
|
||||
background: rgba(34, 34, 34, .95);
|
||||
background: #2F3336;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
|
||||
9
frontend/components/Editor/headingToSlug.js
Normal file
9
frontend/components/Editor/headingToSlug.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
import { escape } from 'lodash';
|
||||
import type { Node } from './types';
|
||||
import slug from 'slug';
|
||||
|
||||
export default function headingToSlug(node: Node) {
|
||||
const level = node.type.replace('heading', 'h');
|
||||
return escape(`${level}-${slug(node.text)}-${node.key}`);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default async function insertImageFile(
|
||||
try {
|
||||
// load the file as a data URL
|
||||
const id = uuid.v4();
|
||||
const alt = '';
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const src = reader.result;
|
||||
@@ -24,7 +25,7 @@ export default async function insertImageFile(
|
||||
.insertBlock({
|
||||
type: 'image',
|
||||
isVoid: true,
|
||||
data: { src, id, loading: true },
|
||||
data: { src, id, alt, loading: true },
|
||||
})
|
||||
.apply();
|
||||
editor.onChange(state);
|
||||
@@ -45,7 +46,7 @@ export default async function insertImageFile(
|
||||
);
|
||||
|
||||
return finalTransform.setNodeByKey(placeholder.key, {
|
||||
data: { src, loading: false },
|
||||
data: { src, alt, loading: false },
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
||||
@@ -73,8 +73,22 @@ export default function MarkdownShortcuts() {
|
||||
let { mark, shortcut } = key;
|
||||
let inlineTags = [];
|
||||
|
||||
// only add tags if they have spaces around them or the tag is beginning or the end of the block
|
||||
for (let i = 0; i < startBlock.text.length; i++) {
|
||||
if (startBlock.text.slice(i, i + shortcut.length) === shortcut)
|
||||
const { text } = startBlock;
|
||||
const start = i;
|
||||
const end = i + shortcut.length;
|
||||
const beginningOfBlock = start === 0;
|
||||
const endOfBlock = end === text.length;
|
||||
const surroundedByWhitespaces = [
|
||||
text.slice(start - 1, start),
|
||||
text.slice(end, end + 1),
|
||||
].includes(' ');
|
||||
|
||||
if (
|
||||
text.slice(start, end) === shortcut &&
|
||||
(beginningOfBlock || endOfBlock || surroundedByWhitespaces)
|
||||
)
|
||||
inlineTags.push(i);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export type Editor = {
|
||||
export type Node = {
|
||||
key: string,
|
||||
kind: string,
|
||||
type: string,
|
||||
length: number,
|
||||
text: string,
|
||||
data: Map<string, any>,
|
||||
|
||||
@@ -9,6 +9,7 @@ export type Props = {
|
||||
primary?: boolean,
|
||||
color?: string,
|
||||
size?: number,
|
||||
onClick?: Function,
|
||||
};
|
||||
|
||||
type BaseProps = {
|
||||
@@ -18,6 +19,7 @@ type BaseProps = {
|
||||
export default function Icon({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
...rest
|
||||
}: Props & BaseProps) {
|
||||
const size = rest.size ? rest.size + 'px' : '24px';
|
||||
@@ -36,6 +38,7 @@ export default function Icon({
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
|
||||
15
frontend/components/Icon/NextIcon.js
Normal file
15
frontend/components/Icon/NextIcon.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import type { Props } from './Icon';
|
||||
|
||||
export default function NextIcon(props: Props) {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<path
|
||||
d="M9.29289322,16.2928932 C8.90236893,16.6834175 8.90236893,17.3165825 9.29289322,17.7071068 C9.68341751,18.0976311 10.3165825,18.0976311 10.7071068,17.7071068 L15.7071068,12.7071068 C16.0976311,12.3165825 16.0976311,11.6834175 15.7071068,11.2928932 L10.7071068,6.29289322 C10.3165825,5.90236893 9.68341751,5.90236893 9.29289322,6.29289322 C8.90236893,6.68341751 8.90236893,7.31658249 9.29289322,7.70710678 L13.5857864,12 L9.29289322,16.2928932 Z"
|
||||
id="path-1"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -182,27 +182,27 @@ const DocumentLink = observer(
|
||||
>
|
||||
<SidebarLink
|
||||
to={document.url}
|
||||
hasChildren={document.children.length > 0}
|
||||
expanded={showChildren}
|
||||
expand={showChildren}
|
||||
expandedContent={
|
||||
document.children.length
|
||||
? <Children column>
|
||||
{document.children.map(childDocument => (
|
||||
<DocumentLink
|
||||
key={childDocument.id}
|
||||
history={history}
|
||||
document={childDocument}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</Children>
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{document.title}
|
||||
</SidebarLink>
|
||||
</DropToImport>
|
||||
|
||||
{showChildren &&
|
||||
<Children column>
|
||||
{document.children &&
|
||||
document.children.map(childDocument => (
|
||||
<DocumentLink
|
||||
key={childDocument.id}
|
||||
history={history}
|
||||
document={childDocument}
|
||||
activeDocument={activeDocument}
|
||||
prefetchDocument={prefetchDocument}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</Children>}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { observable, action } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { color, fontWeight } from 'styles/constants';
|
||||
import styled from 'styled-components';
|
||||
@@ -54,22 +56,54 @@ type Props = {
|
||||
onClick?: SyntheticEvent => *,
|
||||
children?: React$Element<*>,
|
||||
icon?: React$Element<*>,
|
||||
hasChildren?: boolean,
|
||||
expanded?: boolean,
|
||||
expand?: boolean,
|
||||
expandedContent?: React$Element<*>,
|
||||
};
|
||||
|
||||
function SidebarLink({ icon, children, expanded, ...rest }: Props) {
|
||||
const Component = styleComponent(rest.to ? NavLink : StyleableDiv);
|
||||
@observer class SidebarLink extends Component {
|
||||
props: Props;
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<Component exact activeStyle={activeStyle} {...rest}>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
{rest.hasChildren && <StyledGoTo expanded={expanded} />}
|
||||
<Content>{children}</Content>
|
||||
</Component>
|
||||
</Flex>
|
||||
);
|
||||
componentDidMount() {
|
||||
if (this.props.expand) this.handleExpand();
|
||||
}
|
||||
|
||||
componentDidReceiveProps(nextProps: Props) {
|
||||
if (nextProps.expand) this.handleExpand();
|
||||
}
|
||||
|
||||
@observable expanded: boolean = false;
|
||||
|
||||
@action handleClick = (event: SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.expanded = !this.expanded;
|
||||
};
|
||||
|
||||
@action handleExpand = () => {
|
||||
this.expanded = true;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { icon, children, expandedContent, ...rest } = this.props;
|
||||
const Component = styleComponent(rest.to ? NavLink : StyleableDiv);
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<Component
|
||||
exact
|
||||
activeStyle={activeStyle}
|
||||
hasChildren={expandedContent}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
{expandedContent &&
|
||||
<StyledGoTo expanded={this.expanded} onClick={this.handleClick} />}
|
||||
<Content onClick={this.handleExpand}>{children}</Content>
|
||||
</Component>
|
||||
{this.expanded && expandedContent}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Content = styled.div`
|
||||
|
||||
Reference in New Issue
Block a user