diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 922dc5f0..7c4d27a3 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -1,5 +1,6 @@ // @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'; @@ -9,6 +10,7 @@ import Flex from 'components/Flex'; import ClickablePadding from './components/ClickablePadding'; import Toolbar from './components/Toolbar'; import Placeholder from './components/Placeholder'; +import Minimap from './components/Minimap'; import Markdown from './serializer'; import createSchema from './schema'; import createPlugins from './plugins'; @@ -36,10 +38,7 @@ type KeyData = { editor: EditorType; schema: Object; plugins: Array; - - state: { - state: State, - }; + @observable editorState: State; constructor(props: Props) { super(props); @@ -51,9 +50,9 @@ type KeyData = { }); if (props.text) { - this.state = { state: Markdown.deserialize(props.text) }; + this.editorState = Markdown.deserialize(props.text); } else { - this.state = { state: Plain.deserialize('') }; + this.editorState = Plain.deserialize(''); } } @@ -73,12 +72,12 @@ type KeyData = { } } - onChange = (state: State) => { - this.setState({ state }); + onChange = (editorState: State) => { + this.editorState = editorState; }; - onDocumentChange = (document: Document, state: State) => { - this.props.onChange(Markdown.serialize(state)); + onDocumentChange = (document: Document, editorState: State) => { + this.props.onChange(Markdown.serialize(editorState)); }; handleDrop = async (ev: SyntheticEvent) => { @@ -161,7 +160,7 @@ type KeyData = { const transform = state.transform(); transform.collapseToStartOf(state.document); transform.focus(); - this.setState({ state: transform.apply() }); + this.editorState = transform.apply(); }; focusAtEnd = () => { @@ -169,7 +168,7 @@ type KeyData = { const transform = state.transform(); transform.collapseToEndOf(state.document); transform.focus(); - this.setState({ state: transform.apply() }); + this.editorState = transform.apply(); }; render = () => { @@ -184,7 +183,8 @@ type KeyData = { >
- + + (this.editor = ref)} placeholder="Start with a title…" @@ -192,7 +192,7 @@ type KeyData = { schema={this.schema} plugins={this.plugins} emoji={this.props.emoji} - state={this.state.state} + state={this.editorState} onKeyDown={this.onKeyDown} onChange={this.onChange} onDocumentChange={this.onDocumentChange} diff --git a/frontend/components/Editor/components/Heading.js b/frontend/components/Editor/components/Heading.js index c8ceb446..0a9891d9 100644 --- a/frontend/components/Editor/components/Heading.js +++ b/frontend/components/Editor/components/Heading.js @@ -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, + 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.type, node.text); 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 ( - - {children} + + + {children} + {showPlaceholder && {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,28 @@ const Anchor = styled.a` } `; -export const Heading1 = styled(Heading)` +export const StyledHeading = styled(Heading)` position: relative; &:hover { - ${Anchor} { - visibility: visible; - } + ${Anchor} { visibility: visible; } } `; -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) => ( + +); +export const Heading2 = (props: Props) => ( + +); +export const Heading3 = (props: Props) => ( + +); +export const Heading4 = (props: Props) => ( + +); +export const Heading5 = (props: Props) => ( + +); +export const Heading6 = (props: Props) => ( + +); diff --git a/frontend/components/Editor/components/Minimap.js b/frontend/components/Editor/components/Minimap.js new file mode 100644 index 00000000..3d4e096e --- /dev/null +++ b/frontend/components/Editor/components/Minimap.js @@ -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 { + const { state } = this.props; + + return state.document.nodes.filter((node: Block) => { + if (!node.text) return false; + return node.type.match(/^heading/); + }); + } + + render() { + return ( + + + {this.headings.map(heading => ( +
  • + + {heading.text} + +
  • + ))} +
    +
    + ); + } +} + +const Headings = styled.ol` + margin: 0; + padding: 0; +`; + +const Wrapper = styled.div` + position: fixed; + left: 0; + top: 50%; +`; + +export default Minimap; diff --git a/frontend/components/Editor/headingToSlug.js b/frontend/components/Editor/headingToSlug.js new file mode 100644 index 00000000..1eee75dc --- /dev/null +++ b/frontend/components/Editor/headingToSlug.js @@ -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)}`); +} diff --git a/frontend/components/Editor/types.js b/frontend/components/Editor/types.js index 0db4d68a..4406f20f 100644 --- a/frontend/components/Editor/types.js +++ b/frontend/components/Editor/types.js @@ -66,6 +66,7 @@ export type Editor = { export type Node = { key: string, kind: string, + type: string, length: number, text: string, data: Map,