migrate visual editor from prosemirror to slate
This commit is contained in:
parent
49b3a62823
commit
9c869be8fa
@ -15,8 +15,6 @@ collections: # A list of collections the CMS should be able to edit
|
|||||||
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
|
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
|
||||||
- {label: "Cover Image", name: "image", widget: "image", required: false, tagname: ""}
|
- {label: "Cover Image", name: "image", widget: "image", required: false, tagname: ""}
|
||||||
- {label: "Body", name: "body", widget: "markdown"}
|
- {label: "Body", name: "body", widget: "markdown"}
|
||||||
- {label: "Body B", name: "bodyb", widget: "markdown"}
|
|
||||||
- {label: "Body C", name: "bodyc", widget: "markdown"}
|
|
||||||
meta:
|
meta:
|
||||||
- {label: "SEO Description", name: "description", widget: "text"}
|
- {label: "SEO Description", name: "description", widget: "text"}
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@
|
|||||||
"remark-stringify": "^3.0.1",
|
"remark-stringify": "^3.0.1",
|
||||||
"selection-position": "^1.0.0",
|
"selection-position": "^1.0.0",
|
||||||
"semaphore": "^1.0.5",
|
"semaphore": "^1.0.5",
|
||||||
"slate": "^0.14.14",
|
"slate": "^0.20.3",
|
||||||
"slate-drop-or-paste-images": "^0.2.0",
|
"slate-drop-or-paste-images": "^0.2.0",
|
||||||
"slug": "^0.9.1",
|
"slug": "^0.9.1",
|
||||||
"textarea-caret-position": "^0.1.1",
|
"textarea-caret-position": "^0.1.1",
|
||||||
|
@ -332,11 +332,13 @@ export default class RawEditor extends React.Component {
|
|||||||
>
|
>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
selectionPosition={selectionPosition}
|
selectionPosition={selectionPosition}
|
||||||
onH1={this.handleHeader('#')}
|
buttons={{
|
||||||
onH2={this.handleHeader('##')}
|
h1: { onAction: this.handleHeader('#') },
|
||||||
onBold={this.handleBold}
|
h2: { onAction: this.handleHeader('##') },
|
||||||
onItalic={this.handleItalic}
|
bold: { onAction: this.handleBold },
|
||||||
onLink={this.handleLink}
|
italic: { onAction: this.handleItalic },
|
||||||
|
link: { onAction: this.handleLink },
|
||||||
|
}}
|
||||||
onToggleMode={this.handleToggle}
|
onToggleMode={this.handleToggle}
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
onSubmit={this.handlePluginSubmit}
|
onSubmit={this.handlePluginSubmit}
|
||||||
|
@ -10,12 +10,7 @@ import styles from './Toolbar.css';
|
|||||||
|
|
||||||
export default class Toolbar extends React.Component {
|
export default class Toolbar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
selectionPosition: PropTypes.object,
|
buttons: PropTypes.object.isRequired,
|
||||||
onH1: PropTypes.func.isRequired,
|
|
||||||
onH2: PropTypes.func.isRequired,
|
|
||||||
onBold: PropTypes.func.isRequired,
|
|
||||||
onItalic: PropTypes.func.isRequired,
|
|
||||||
onLink: PropTypes.func.isRequired,
|
|
||||||
onToggleMode: PropTypes.func.isRequired,
|
onToggleMode: PropTypes.func.isRequired,
|
||||||
rawMode: PropTypes.bool,
|
rawMode: PropTypes.bool,
|
||||||
plugins: ImmutablePropTypes.map,
|
plugins: ImmutablePropTypes.map,
|
||||||
@ -47,11 +42,7 @@ export default class Toolbar extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
onH1,
|
buttons,
|
||||||
onH2,
|
|
||||||
onBold,
|
|
||||||
onItalic,
|
|
||||||
onLink,
|
|
||||||
onToggleMode,
|
onToggleMode,
|
||||||
rawMode,
|
rawMode,
|
||||||
plugins,
|
plugins,
|
||||||
@ -62,13 +53,19 @@ export default class Toolbar extends React.Component {
|
|||||||
|
|
||||||
const { activePlugin } = this.state;
|
const { activePlugin } = this.state;
|
||||||
|
|
||||||
|
const buttonsConfig = [
|
||||||
|
{ label: 'Header 1', icon: 'h1', state: buttons.h1 },
|
||||||
|
{ label: 'Header 2', icon: 'h2', state: buttons.h2 },
|
||||||
|
{ label: 'Bold', icon: 'bold', state: buttons.bold },
|
||||||
|
{ label: 'Italic', icon: 'italic', state: buttons.italic },
|
||||||
|
{ label: 'Link', icon: 'link', state: buttons.link },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.Toolbar}>
|
<div className={styles.Toolbar}>
|
||||||
<ToolbarButton label="Header 1" icon="h1" action={onH1}/>
|
{ buttonsConfig.map((btn, i) => (
|
||||||
<ToolbarButton label="Header 2" icon="h2" action={onH2}/>
|
<ToolbarButton key={i} action={btn.state.onAction} active={btn.state.active} {...btn}/>
|
||||||
<ToolbarButton label="Bold" icon="bold" action={onBold}/>
|
))}
|
||||||
<ToolbarButton label="Italic" icon="italic" action={onItalic}/>
|
|
||||||
<ToolbarButton label="Link" icon="link" action={onLink}/>
|
|
||||||
<ToolbarComponentsMenu
|
<ToolbarComponentsMenu
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
onComponentMenuItemClick={this.handlePluginFormDisplay}
|
onComponentMenuItemClick={this.handlePluginFormDisplay}
|
||||||
|
@ -68,86 +68,38 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global {
|
.slateEditor {
|
||||||
& .ProseMirror {
|
position: relative;
|
||||||
position: relative;
|
background-color: var(--controlBGColor);
|
||||||
background-color: var(--controlBGColor);
|
padding: 12px;
|
||||||
padding: 12px;
|
overflow: hidden;
|
||||||
overflow: hidden;
|
border-radius: var(--borderRadius);
|
||||||
border-radius: var(--borderRadius);
|
overflow-x: auto;
|
||||||
overflow-x: auto;
|
border: var(--textFieldBorder);
|
||||||
border: var(--textFieldBorder);
|
min-height: var(--richTextEditorMinHeight);
|
||||||
min-height: var(--richTextEditorMinHeight);
|
|
||||||
|
|
||||||
& ul,
|
& ul,
|
||||||
& ol {
|
& ol {
|
||||||
padding-left: 20px;
|
padding-left: 30px;
|
||||||
}
|
|
||||||
|
|
||||||
& pre > code {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
background-color: #000;
|
|
||||||
color: #ccc;
|
|
||||||
border-radius: var(--borderRadius);
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ProseMirror-content {
|
& pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ProseMirror-drop-target {
|
& pre > code {
|
||||||
position: absolute;
|
display: block;
|
||||||
width: 1px;
|
width: 100%;
|
||||||
background: #666;
|
overflow-y: auto;
|
||||||
pointer-events: none;
|
background-color: #000;
|
||||||
|
color: #ccc;
|
||||||
|
border-radius: var(--borderRadius);
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ProseMirror-content ul, & .ProseMirror-content ol {
|
& blockquote {
|
||||||
padding-left: 30px;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .ProseMirror-content blockquote {
|
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
border-left: 3px solid #eee;
|
border-left: 3px solid #eee;
|
||||||
margin-left: 0; margin-right: 0;
|
margin-left: 0; margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ProseMirror-content pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .ProseMirror-content li {
|
|
||||||
position: relative;
|
|
||||||
pointer-events: none; /* Don't do weird stuff with marker clicks */
|
|
||||||
}
|
|
||||||
& .ProseMirror-content li > * {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .ProseMirror-nodeselection *::selection { background: transparent; }
|
|
||||||
& .ProseMirror-nodeselection *::-moz-selection { background: transparent; }
|
|
||||||
|
|
||||||
& .ProseMirror-selectednode {
|
|
||||||
outline: 2px solid #8cf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make sure li selections wrap around markers */
|
|
||||||
|
|
||||||
& li.ProseMirror-selectednode {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& li.ProseMirror-selectednode:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: -32px;
|
|
||||||
right: -2px; top: -2px; bottom: -2px;
|
|
||||||
border: 2px solid #8cf;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { Map } from 'immutable';
|
import { Map, List } from 'immutable';
|
||||||
import { Schema } from 'prosemirror-model';
|
import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw} from 'slate';
|
||||||
import { EditorState } from 'prosemirror-state';
|
|
||||||
import { EditorView } from 'prosemirror-view';
|
|
||||||
import history from 'prosemirror-history';
|
|
||||||
import {
|
|
||||||
blockQuoteRule, orderedListRule, bulletListRule, codeBlockRule, headingRule,
|
|
||||||
inputRules, allInputRules,
|
|
||||||
} from 'prosemirror-inputrules';
|
|
||||||
import { keymap } from 'prosemirror-keymap';
|
|
||||||
import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
|
||||||
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
|
||||||
import unified from 'unified';
|
import unified from 'unified';
|
||||||
import markdownToRemark from 'remark-parse';
|
import markdownToRemark from 'remark-parse';
|
||||||
|
import remarkToRehype from 'remark-rehype';
|
||||||
|
import rehypeToHtml from 'rehype-stringify';
|
||||||
import remarkToMarkdown from 'remark-stringify';
|
import remarkToMarkdown from 'remark-stringify';
|
||||||
|
import htmlToRehype from 'rehype-parse';
|
||||||
|
import rehypeToRemark from 'rehype-remark';
|
||||||
import registry from '../../../../lib/registry';
|
import registry from '../../../../lib/registry';
|
||||||
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||||
import { buildKeymap } from './keymap';
|
import { buildKeymap } from './keymap';
|
||||||
@ -32,28 +26,7 @@ function processUrl(url) {
|
|||||||
return `/${ url }`;
|
return `/${ url }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ruleset = {
|
const DEFAULT_NODE = 'paragraph';
|
||||||
blockquote: [blockQuoteRule],
|
|
||||||
ordered_list: [orderedListRule],
|
|
||||||
bullet_list: [bulletListRule],
|
|
||||||
code_block: [codeBlockRule],
|
|
||||||
heading: [headingRule, 6],
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildInputRules(schema) {
|
|
||||||
return Map(ruleset)
|
|
||||||
.filter(rule => schema.nodes[rule])
|
|
||||||
.map(rule => rule[0].apply(rule[0].slice(1)))
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
function markActive(state, type) {
|
|
||||||
const { from, to, empty, $from } = state.selection;
|
|
||||||
if (empty) {
|
|
||||||
return type.isInSet(state.storedMarks || $from.marks());
|
|
||||||
}
|
|
||||||
return state.doc.rangeHasMark(from, to, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function schemaWithPlugins(schema, plugins) {
|
function schemaWithPlugins(schema, plugins) {
|
||||||
let nodeSpec = schema.nodeSpec;
|
let nodeSpec = schema.nodeSpec;
|
||||||
@ -94,115 +67,229 @@ function createSerializer(schema, plugins) {
|
|||||||
return serializer;
|
return serializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BLOCK_TAGS = {
|
||||||
|
p: 'paragraph',
|
||||||
|
li: 'list-item',
|
||||||
|
ul: 'bulleted-list',
|
||||||
|
ol: 'numbered-list',
|
||||||
|
blockquote: 'quote',
|
||||||
|
pre: 'code',
|
||||||
|
h1: 'heading-one',
|
||||||
|
h2: 'heading-two',
|
||||||
|
h3: 'heading-three',
|
||||||
|
h4: 'heading-four',
|
||||||
|
h5: 'heading-five',
|
||||||
|
h6: 'heading-six'
|
||||||
|
}
|
||||||
|
|
||||||
|
const MARK_TAGS = {
|
||||||
|
strong: 'bold',
|
||||||
|
em: 'italic',
|
||||||
|
u: 'underline',
|
||||||
|
s: 'strikethrough',
|
||||||
|
code: 'code'
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_COMPONENTS = {
|
||||||
|
'quote': props => <blockquote {...props.attributes}>{props.children}</blockquote>,
|
||||||
|
'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
|
||||||
|
'heading-one': props => <h1 {...props.attributes}>{props.children}</h1>,
|
||||||
|
'heading-two': props => <h2 {...props.attributes}>{props.children}</h2>,
|
||||||
|
'heading-three': props => <h3 {...props.attributes}>{props.children}</h3>,
|
||||||
|
'heading-four': props => <h4 {...props.attributes}>{props.children}</h4>,
|
||||||
|
'heading-five': props => <h5 {...props.attributes}>{props.children}</h5>,
|
||||||
|
'heading-six': props => <h6 {...props.attributes}>{props.children}</h6>,
|
||||||
|
'list-item': props => <li {...props.attributes}>{props.children}</li>,
|
||||||
|
'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
|
||||||
|
'code': props => <pre {...props.attributes}><code>{props.children}</code></pre>,
|
||||||
|
'link': props => <a href={props.node.data.href} {...props.attributes}>{props.children}</a>,
|
||||||
|
'paragraph': props => <p>{props.children}</p>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MARK_COMPONENTS = {
|
||||||
|
bold: props => <strong>{props.children}</strong>,
|
||||||
|
code: props => <code>{props.children}</code>,
|
||||||
|
italic: props => <em>{props.children}</em>,
|
||||||
|
underlined: props => <u>{props.children}</u>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RULES = [
|
||||||
|
{
|
||||||
|
deserialize(el, next) {
|
||||||
|
const block = BLOCK_TAGS[el.tagName]
|
||||||
|
if (!block) return
|
||||||
|
return {
|
||||||
|
kind: 'block',
|
||||||
|
type: block,
|
||||||
|
nodes: next(el.children)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serialize(entity, children) {
|
||||||
|
const component = NODE_COMPONENTS[entity.type]
|
||||||
|
if (!component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return component({ children });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deserialize(el, next) {
|
||||||
|
const mark = MARK_TAGS[el.tagName]
|
||||||
|
if (!mark) return
|
||||||
|
return {
|
||||||
|
kind: 'mark',
|
||||||
|
type: mark,
|
||||||
|
nodes: next(el.children)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serialize(entity, children) {
|
||||||
|
const component = MARK_COMPONENTS[entity.type]
|
||||||
|
if (!component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return component({ children });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Special case for code blocks, which need to grab the nested children.
|
||||||
|
deserialize(el, next) {
|
||||||
|
if (el.tagName != 'pre') return
|
||||||
|
const code = el.children[0]
|
||||||
|
const children = code && code.tagName == 'code'
|
||||||
|
? code.children
|
||||||
|
: el.children
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'block',
|
||||||
|
type: 'code',
|
||||||
|
nodes: next(children)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Special case for links, to grab their href.
|
||||||
|
deserialize(el, next) {
|
||||||
|
if (el.tagName != 'a') return
|
||||||
|
return {
|
||||||
|
kind: 'inline',
|
||||||
|
type: 'link',
|
||||||
|
nodes: next(el.children),
|
||||||
|
data: {
|
||||||
|
href: el.attribs.href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const serializer = new SlateHtml({ rules: RULES });
|
||||||
|
|
||||||
export default class Editor extends Component {
|
export default class Editor extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const plugins = registry.getEditorComponents();
|
const plugins = registry.getEditorComponents();
|
||||||
const schema = schemaWithPlugins(markdownSchema, plugins);
|
const html = unified()
|
||||||
|
.use(markdownToRemark)
|
||||||
|
.use(remarkToRehype)
|
||||||
|
.use(rehypeToHtml)
|
||||||
|
.processSync(this.props.value || '')
|
||||||
|
.contents;
|
||||||
this.state = {
|
this.state = {
|
||||||
|
editorState: serializer.deserialize(html),
|
||||||
|
schema: {
|
||||||
|
nodes: NODE_COMPONENTS,
|
||||||
|
marks: MARK_COMPONENTS,
|
||||||
|
},
|
||||||
plugins,
|
plugins,
|
||||||
schema,
|
|
||||||
parser: createMarkdownParser(schema, plugins),
|
|
||||||
serializer: createSerializer(schema, plugins),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
handleDocumentChange = (doc, editorState) => {
|
||||||
this.view = new EditorView(this.ref, {
|
const html = serializer.serialize(editorState);
|
||||||
state: this.createEditorState(),
|
const markdown = unified()
|
||||||
onAction: this.handleAction,
|
.use(htmlToRehype)
|
||||||
dispatchTransaction: this.handleTransaction,
|
.use(rehypeToRemark)
|
||||||
});
|
.use(remarkToMarkdown)
|
||||||
}
|
.processSync(html)
|
||||||
|
.contents;
|
||||||
createEditorState() {
|
this.props.onChange(markdown);
|
||||||
const { schema, parser } = this.state;
|
|
||||||
const doc = parser.parse(this.props.value || '');
|
|
||||||
|
|
||||||
return EditorState.create({
|
|
||||||
doc,
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const editorValue = this.state.serializer.serialize(this.view.state.doc);
|
|
||||||
// Check that the content of the editor is well synchronized with the props value after rendering.
|
|
||||||
// Sometimes the editor isn't well updated (eg. after items reordering)
|
|
||||||
if (editorValue !== this.props.value && editorValue !== prevProps.value) {
|
|
||||||
// If the content of the editor isn't correct, we update its state with a new one.
|
|
||||||
this.view.updateState(this.createEditorState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTransaction = (transaction) => {
|
|
||||||
const { serializer } = this.state;
|
|
||||||
const newState = this.view.state.apply(transaction);
|
|
||||||
const md = serializer.serialize(newState.doc);
|
|
||||||
const processedMarkdown = unified()
|
|
||||||
.use(markdownToRemark)
|
|
||||||
.use(remarkToMarkdown, { fences: true, commonmark: true, footnotes: true, pedantic: true })
|
|
||||||
.processSync(md);
|
|
||||||
this.props.onChange(processedMarkdown.contents);
|
|
||||||
this.view.updateState(newState);
|
|
||||||
if (newState.selection !== this.state.selection) {
|
|
||||||
this.handleSelection(newState);
|
|
||||||
}
|
|
||||||
this.view.focus();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSelection = (state) => {
|
hasMark = type => this.state.editorState.marks.some(mark => mark.type === type);
|
||||||
const { schema, selection } = state;
|
hasBlock = type => this.state.editorState.blocks.some(node => node.type === type);
|
||||||
if (selection.from === selection.to) {
|
|
||||||
const { $from } = selection;
|
handleKeyDown = (e, data, state) => {
|
||||||
if ($from.parent && $from.parent.type === schema.nodes.paragraph && $from.parent.textContent === '') {
|
if (!data.isMod) {
|
||||||
const pos = this.view.coordsAtPos(selection.from);
|
return;
|
||||||
const editorPos = this.view.content.getBoundingClientRect();
|
}
|
||||||
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
const marks = {
|
||||||
this.setState({ selectionPosition });
|
b: 'bold',
|
||||||
|
i: 'italic',
|
||||||
|
u: 'underlined',
|
||||||
|
'`': 'code',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mark = marks[data.key];
|
||||||
|
|
||||||
|
if (mark) {
|
||||||
|
state = state.transform().toggleMark(mark).apply();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMarkClick = (event, type) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const resolvedState = this.state.editorState.transform().toggleMark(type).apply();
|
||||||
|
this.ref.onChange(resolvedState);
|
||||||
|
this.setState({ editorState: resolvedState });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBlockClick = (event, type) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let { editorState } = this.state;
|
||||||
|
const transform = editorState.transform();
|
||||||
|
const doc = editorState.document;
|
||||||
|
const isList = this.hasBlock('list-item')
|
||||||
|
|
||||||
|
// Handle everything except list buttons.
|
||||||
|
if (!['bulleted-list', 'numbered-list'].includes(type)) {
|
||||||
|
const isActive = this.hasBlock(type);
|
||||||
|
const transformed = transform.setBlock(isActive ? DEFAULT_NODE : type);
|
||||||
|
|
||||||
|
if (isList) {
|
||||||
|
transformed
|
||||||
|
.unwrapBlock('bulleted-list')
|
||||||
|
.unwrapBlock('numbered-list');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const pos = this.view.coordsAtPos(selection.from);
|
|
||||||
const editorPos = this.view.content.getBoundingClientRect();
|
|
||||||
const selectionPosition = { top: pos.top - editorPos.top, left: pos.left - editorPos.left };
|
|
||||||
this.setState({ selectionPosition });
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
handleRef = (ref) => {
|
// Handle the extra wrapping required for list buttons.
|
||||||
this.ref = ref;
|
else {
|
||||||
};
|
const isType = editorState.blocks.some(block => {
|
||||||
|
return !!doc.getClosest(block.key, parent => parent.type === type);
|
||||||
|
});
|
||||||
|
|
||||||
handleHeader = level => (
|
if (isList && isType) {
|
||||||
() => {
|
transform
|
||||||
const { schema } = this.state;
|
.setBlock(DEFAULT_NODE)
|
||||||
const state = this.view.state;
|
.unwrapBlock('bulleted-list')
|
||||||
const { $from, to, node } = state.selection;
|
.unwrapBlock('numbered-list');
|
||||||
let nodeType = schema.nodes.heading;
|
} else if (isList) {
|
||||||
let attrs = { level };
|
transform
|
||||||
let inHeader = node && node.hasMarkup(nodeType, attrs);
|
.unwrapBlock(type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list')
|
||||||
if (!inHeader) {
|
.wrapBlock(type);
|
||||||
inHeader = to <= $from.end() && $from.parent.hasMarkup(nodeType, attrs);
|
} else {
|
||||||
|
transform
|
||||||
|
.setBlock('list-item')
|
||||||
|
.wrapBlock(type);
|
||||||
}
|
}
|
||||||
if (inHeader) {
|
|
||||||
nodeType = schema.nodes.paragraph;
|
|
||||||
attrs = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = setBlockType(nodeType, { level });
|
|
||||||
command(state, this.handleAction);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
handleBold = () => {
|
const resolvedState = transform.focus().apply();
|
||||||
const command = toggleMark(this.state.schema.marks.strong);
|
this.ref.onChange(resolvedState);
|
||||||
command(this.view.state, this.handleAction);
|
this.setState({ editorState: resolvedState });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleItalic = () => {
|
|
||||||
const command = toggleMark(this.state.schema.marks.em);
|
|
||||||
command(this.view.state, this.handleAction);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLink = () => {
|
handleLink = () => {
|
||||||
let url = null;
|
let url = null;
|
||||||
@ -216,7 +303,7 @@ export default class Editor extends Component {
|
|||||||
handlePluginSubmit = (plugin, data) => {
|
handlePluginSubmit = (plugin, data) => {
|
||||||
const { schema } = this.state;
|
const { schema } = this.state;
|
||||||
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
|
||||||
this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action());
|
//this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action());
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDragEnter = (e) => {
|
handleDragEnter = (e) => {
|
||||||
@ -263,7 +350,7 @@ export default class Editor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action());
|
//this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -271,6 +358,12 @@ export default class Editor extends Component {
|
|||||||
this.props.onMode('raw');
|
this.props.onMode('raw');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getButtonProps = (type, isBlock) => {
|
||||||
|
const handler = isBlock ? this.handleBlockClick: this.handleMarkClick;
|
||||||
|
const isActive = isBlock ? this.hasBlock : this.hasMark;
|
||||||
|
return { onAction: e => handler(e, type), active: isActive(type) };
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
|
||||||
const { plugins, selectionPosition, dragging } = this.state;
|
const { plugins, selectionPosition, dragging } = this.state;
|
||||||
@ -293,11 +386,13 @@ export default class Editor extends Component {
|
|||||||
>
|
>
|
||||||
<Toolbar
|
<Toolbar
|
||||||
selectionPosition={selectionPosition}
|
selectionPosition={selectionPosition}
|
||||||
onH1={this.handleHeader(1)}
|
buttons={{
|
||||||
onH2={this.handleHeader(2)}
|
h1: this.getButtonProps('heading-one', true),
|
||||||
onBold={this.handleBold}
|
h2: this.getButtonProps('heading-two', true),
|
||||||
onItalic={this.handleItalic}
|
bold: this.getButtonProps('bold'),
|
||||||
onLink={this.handleLink}
|
italic: this.getButtonProps('italic'),
|
||||||
|
link: this.getButtonProps('link'),
|
||||||
|
}}
|
||||||
onToggleMode={this.handleToggle}
|
onToggleMode={this.handleToggle}
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
onSubmit={this.handlePluginSubmit}
|
onSubmit={this.handlePluginSubmit}
|
||||||
@ -306,7 +401,16 @@ export default class Editor extends Component {
|
|||||||
getAsset={getAsset}
|
getAsset={getAsset}
|
||||||
/>
|
/>
|
||||||
</Sticky>
|
</Sticky>
|
||||||
<div ref={this.handleRef} />
|
<SlateEditor
|
||||||
|
className={styles.slateEditor}
|
||||||
|
state={this.state.editorState}
|
||||||
|
schema={this.state.schema}
|
||||||
|
onChange={editorState => this.setState({ editorState })}
|
||||||
|
onDocumentChange={this.handleDocumentChange}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
ref={ref => this.ref = ref}
|
||||||
|
spellCheck
|
||||||
|
/>
|
||||||
<div className={styles.shim} />
|
<div className={styles.shim} />
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
function remarkToSlate(opts) {
|
||||||
|
|
||||||
|
console.log(1);
|
||||||
|
return transform;
|
||||||
|
|
||||||
|
function transform(node) {
|
||||||
|
console.log(2);
|
||||||
|
console.log(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default remarkToSlate;
|
@ -0,0 +1,9 @@
|
|||||||
|
function slateToRemark(opts) {
|
||||||
|
return transform;
|
||||||
|
|
||||||
|
function transform(node) {
|
||||||
|
console.log(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default slateToRemark;
|
Loading…
x
Reference in New Issue
Block a user