Functional TOC
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Editor, Plain } from 'slate';
|
import { Editor, Plain } from 'slate';
|
||||||
import keydown from 'react-keydown';
|
import keydown from 'react-keydown';
|
||||||
@ -9,6 +10,7 @@ import Flex from 'components/Flex';
|
|||||||
import ClickablePadding from './components/ClickablePadding';
|
import ClickablePadding from './components/ClickablePadding';
|
||||||
import Toolbar from './components/Toolbar';
|
import Toolbar from './components/Toolbar';
|
||||||
import Placeholder from './components/Placeholder';
|
import Placeholder from './components/Placeholder';
|
||||||
|
import Minimap from './components/Minimap';
|
||||||
import Markdown from './serializer';
|
import Markdown from './serializer';
|
||||||
import createSchema from './schema';
|
import createSchema from './schema';
|
||||||
import createPlugins from './plugins';
|
import createPlugins from './plugins';
|
||||||
@ -36,10 +38,7 @@ type KeyData = {
|
|||||||
editor: EditorType;
|
editor: EditorType;
|
||||||
schema: Object;
|
schema: Object;
|
||||||
plugins: Array<Object>;
|
plugins: Array<Object>;
|
||||||
|
@observable editorState: State;
|
||||||
state: {
|
|
||||||
state: State,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -51,9 +50,9 @@ type KeyData = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (props.text) {
|
if (props.text) {
|
||||||
this.state = { state: Markdown.deserialize(props.text) };
|
this.editorState = Markdown.deserialize(props.text);
|
||||||
} else {
|
} else {
|
||||||
this.state = { state: Plain.deserialize('') };
|
this.editorState = Plain.deserialize('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,12 +72,12 @@ type KeyData = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = (state: State) => {
|
onChange = (editorState: State) => {
|
||||||
this.setState({ state });
|
this.editorState = editorState;
|
||||||
};
|
};
|
||||||
|
|
||||||
onDocumentChange = (document: Document, state: State) => {
|
onDocumentChange = (document: Document, editorState: State) => {
|
||||||
this.props.onChange(Markdown.serialize(state));
|
this.props.onChange(Markdown.serialize(editorState));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDrop = async (ev: SyntheticEvent) => {
|
handleDrop = async (ev: SyntheticEvent) => {
|
||||||
@ -161,7 +160,7 @@ type KeyData = {
|
|||||||
const transform = state.transform();
|
const transform = state.transform();
|
||||||
transform.collapseToStartOf(state.document);
|
transform.collapseToStartOf(state.document);
|
||||||
transform.focus();
|
transform.focus();
|
||||||
this.setState({ state: transform.apply() });
|
this.editorState = transform.apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
focusAtEnd = () => {
|
focusAtEnd = () => {
|
||||||
@ -169,7 +168,7 @@ type KeyData = {
|
|||||||
const transform = state.transform();
|
const transform = state.transform();
|
||||||
transform.collapseToEndOf(state.document);
|
transform.collapseToEndOf(state.document);
|
||||||
transform.focus();
|
transform.focus();
|
||||||
this.setState({ state: transform.apply() });
|
this.editorState = transform.apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
@ -184,7 +183,8 @@ type KeyData = {
|
|||||||
>
|
>
|
||||||
<MaxWidth column auto>
|
<MaxWidth column auto>
|
||||||
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
||||||
<Toolbar state={this.state.state} onChange={this.onChange} />
|
<Toolbar state={this.editorState} onChange={this.onChange} />
|
||||||
|
<Minimap state={this.editorState} />
|
||||||
<StyledEditor
|
<StyledEditor
|
||||||
innerRef={ref => (this.editor = ref)}
|
innerRef={ref => (this.editor = ref)}
|
||||||
placeholder="Start with a title…"
|
placeholder="Start with a title…"
|
||||||
@ -192,7 +192,7 @@ type KeyData = {
|
|||||||
schema={this.schema}
|
schema={this.schema}
|
||||||
plugins={this.plugins}
|
plugins={this.plugins}
|
||||||
emoji={this.props.emoji}
|
emoji={this.props.emoji}
|
||||||
state={this.state.state}
|
state={this.editorState}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onDocumentChange={this.onDocumentChange}
|
onDocumentChange={this.onDocumentChange}
|
||||||
|
@ -2,13 +2,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Document } from 'slate';
|
import { Document } from 'slate';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import _ from 'lodash';
|
import headingToSlug from '../headingToSlug';
|
||||||
import slug from 'slug';
|
|
||||||
import type { Node, Editor } from '../types';
|
import type { Node, Editor } from '../types';
|
||||||
import Placeholder from './Placeholder';
|
import Placeholder from './Placeholder';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React$Element<any>,
|
children: React$Element<*>,
|
||||||
placeholder?: boolean,
|
placeholder?: boolean,
|
||||||
parent: Node,
|
parent: Node,
|
||||||
node: Node,
|
node: Node,
|
||||||
@ -31,7 +30,7 @@ function Heading(props: Props) {
|
|||||||
const parentIsDocument = parent instanceof Document;
|
const parentIsDocument = parent instanceof Document;
|
||||||
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
const slugish = headingToSlug(node.type, node.text);
|
||||||
const showHash = readOnly && !!slugish;
|
const showHash = readOnly && !!slugish;
|
||||||
const Component = component;
|
const Component = component;
|
||||||
const emoji = editor.props.emoji || '';
|
const emoji = editor.props.emoji || '';
|
||||||
@ -40,8 +39,10 @@ function Heading(props: Props) {
|
|||||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component {...rest}>
|
<Component {...rest} id={slugish}>
|
||||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
|
||||||
|
{children}
|
||||||
|
</Wrapper>
|
||||||
{showPlaceholder &&
|
{showPlaceholder &&
|
||||||
<Placeholder contentEditable={false}>
|
<Placeholder contentEditable={false}>
|
||||||
{editor.props.placeholder}
|
{editor.props.placeholder}
|
||||||
@ -53,7 +54,7 @@ function Heading(props: Props) {
|
|||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
margin-left: ${(props: Props) => (props.hasEmoji ? '-1.2em' : 0)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Anchor = styled.a`
|
const Anchor = styled.a`
|
||||||
@ -66,19 +67,28 @@ const Anchor = styled.a`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Heading1 = styled(Heading)`
|
export const StyledHeading = styled(Heading)`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${Anchor} {
|
${Anchor} { visibility: visible; }
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const Heading2 = Heading1.withComponent('h2');
|
export const Heading1 = (props: Props) => (
|
||||||
export const Heading3 = Heading1.withComponent('h3');
|
<StyledHeading component="h1" {...props} />
|
||||||
export const Heading4 = Heading1.withComponent('h4');
|
);
|
||||||
export const Heading5 = Heading1.withComponent('h5');
|
export const Heading2 = (props: Props) => (
|
||||||
export const Heading6 = Heading1.withComponent('h6');
|
<StyledHeading component="h2" {...props} />
|
||||||
|
);
|
||||||
export default Heading;
|
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} />
|
||||||
|
);
|
||||||
|
52
frontend/components/Editor/components/Minimap.js
Normal file
52
frontend/components/Editor/components/Minimap.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
import headingToSlug from '../headingToSlug';
|
||||||
|
import type { State, Block } from '../types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
state: State,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Minimap extends Component {
|
||||||
|
props: Props;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Headings>
|
||||||
|
{this.headings.map(heading => (
|
||||||
|
<li>
|
||||||
|
<a href={`#${headingToSlug(heading.type, heading.text)}`}>
|
||||||
|
{heading.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</Headings>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Headings = styled.ol`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Minimap;
|
8
frontend/components/Editor/headingToSlug.js
Normal file
8
frontend/components/Editor/headingToSlug.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// @flow
|
||||||
|
import { escape } from 'lodash';
|
||||||
|
import slug from 'slug';
|
||||||
|
|
||||||
|
export default function headingToSlug(heading: string, title: string) {
|
||||||
|
const level = heading.replace('heading', 'h');
|
||||||
|
return escape(`${level}-${slug(title)}`);
|
||||||
|
}
|
@ -66,6 +66,7 @@ export type Editor = {
|
|||||||
export type Node = {
|
export type Node = {
|
||||||
key: string,
|
key: string,
|
||||||
kind: string,
|
kind: string,
|
||||||
|
type: string,
|
||||||
length: number,
|
length: number,
|
||||||
text: string,
|
text: string,
|
||||||
data: Map<string, any>,
|
data: Map<string, any>,
|
||||||
|
Reference in New Issue
Block a user