diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css index aa2fec15..30b75f9c 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.css @@ -1,6 +1,6 @@ @import "../../../../UI/theme"; -.root { +.rawWrapper { position: relative; } @@ -12,7 +12,7 @@ composes: editorControlBarSticky from "../VisualEditor/index.css"; } -.SlateEditor { +.rawEditor { position: relative; overflow: hidden; overflow-x: auto; diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 15bef1b6..6615a1f2 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Editor as SlateEditor, Plain as SlatePlain } from 'slate'; +import { Editor as Slate, Plain } from 'slate'; import { markdownToRemark, remarkToMarkdown } from '../../unified'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; @@ -8,32 +8,44 @@ import styles from './index.css'; export default class RawEditor extends React.Component { constructor(props) { super(props); + /** + * The value received is a Remark AST (MDAST), and must be stringified + * to plain text before Slate's Plain serializer can convert it to the + * Slate AST. + */ const value = remarkToMarkdown(this.props.value); this.state = { - editorState: SlatePlain.deserialize(value || ''), + editorState: Plain.deserialize(value || ''), }; } shouldComponentUpdate(nextProps, nextState) { - if (this.state.editorState.equals(nextState.editorState)) { - return false - } - return true; + return !this.state.editorState.equals(nextState.editorState); } handleChange = editorState => { this.setState({ editorState }); } + /** + * When the document value changes, serialize from Slate's AST back to plain + * text (which is Markdown), and then deserialize from that to a Remark MDAST, + * before passing up as the new value. + */ handleDocumentChange = (doc, editorState) => { - const value = SlatePlain.serialize(editorState); - const html = markdownToRemark(value); - this.props.onChange(html); + const value = Plain.serialize(editorState); + const mdast = markdownToRemark(value); + this.props.onChange(mdast); }; + /** + * If a paste contains plain text, deserialize it to Slate's AST and insert + * to the document. Selection logic (where to insert, whether to replace) is + * handled by Slate. + */ handlePaste = (e, data, state) => { if (data.text) { - const fragment = SlatePlain.deserialize(data.text).document; + const fragment = Plain.deserialize(data.text).document; return state.transform().insertFragment(fragment).apply(); } }; @@ -44,7 +56,7 @@ export default class RawEditor extends React.Component { render() { return ( -
{props.children}
,
+};
+
+export const NODE_COMPONENTS = {
+ paragraph: props => {props.children}
, + 'list-item': props =>{props.children}, + code: props =>
{props.children}
,
+ 'heading-one': props => {props.children}
, - 'list-item': props =>{props.children}, - 'code': props =>
{props.children}
,
- 'heading-one': props => {props.children}
,
-};
-
-const SoftBreak = (options = {}) => ({
- onKeyDown(e, data, state) {
- if (data.key != 'enter') return;
- if (options.shift && e.shiftKey == false) return;
-
- const { onlyIn, ignoreIn, closeAfter, unwrapBlocks, defaultBlock = 'paragraph' } = options;
- const { type, nodes } = state.startBlock;
- if (onlyIn && !onlyIn.includes(type)) return;
- if (ignoreIn && ignoreIn.includes(type)) return;
-
- const shouldClose = nodes.last().characters.takeLast(closeAfter).every(c => c.text === '\n');
- if (closeAfter && shouldClose) {
- const trimmed = state.transform().deleteBackward(closeAfter);
- const unwrapped = unwrapBlocks
- ? unwrapBlocks.reduce((acc, blockType) => acc.unwrapBlock(blockType), trimmed)
- : trimmed;
- return unwrapped.insertBlock(defaultBlock).apply();
- }
-
- return state.transform().insertText('\n').apply();
- }
-});
-
-const BackspaceCloseBlock = (options = {}) => ({
- onKeyDown(e, data, state) {
- if (data.key != 'backspace') return;
-
- const { defaultBlock = 'paragraph', ignoreIn, onlyIn } = options;
- const { startBlock } = state;
- const { type } = startBlock;
-
- if (onlyIn && !onlyIn.includes(type)) return;
- if (ignoreIn && ignoreIn.includes(type)) return;
-
- const characters = startBlock.getFirstText().characters;
- const isEmpty = !characters || characters.isEmpty();
-
- if (isEmpty) {
- return state.transform().insertBlock(defaultBlock).focus().apply();
- }
- }
-});
-
-const EditListPlugin = EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' });
-
-const slatePlugins = [
- SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }),
- BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list', 'table', 'table-row', 'table-cell'] }),
- EditListPlugin,
- EditTable({ typeTable: 'table', typeRow: 'table-row', typeCell: 'table-cell' }),
-];
-
export default class Editor extends Component {
constructor(props) {
super(props);
- const plugins = registry.getEditorComponents();
- const emptyRaw = {
- nodes: [{ kind: 'block', type: 'paragraph', nodes: [
- { kind: 'text', ranges: [{ text: '' }] }
- ]}],
- };
- const remark = this.props.value && remarkToSlate(this.props.value);
- const initialValue = get(remark, ['nodes', 'length']) ? remark : emptyRaw;
- const editorState = SlateRaw.deserialize(initialValue, { terse: true });
+ const emptyBlock = Block.create({ kind: 'block', type: 'paragraph'});
+ const emptyRaw = { nodes: [emptyBlock] };
+ const mdast = this.props.value && remarkToSlate(this.props.value);
+ const mdastHasNodes = !isEmpty(get(mdast, 'nodes'))
+ const editorState = Raw.deserialize(mdastHasNodes ? mdast : emptyRaw, { terse: true });
this.state = {
editorState,
schema: {
nodes: NODE_COMPONENTS,
marks: MARK_COMPONENTS,
- rules: [
- /**
- * If the editor is ever in an empty state, insert an empty
- * paragraph block.
- */
- {
- match: object => object.kind === 'document',
- validate: doc => {
- const hasBlocks = !doc.getBlocks().isEmpty();
- return hasBlocks ? null : {};
- },
- normalize: transform => {
- const block = SlateBlock.create({
- type: 'paragraph',
- nodes: [SlateText.createFromString('')],
- });
- const { key } = transform.state.document;
- return transform.insertNodeByKey(key, 0, block).focus();
- },
- },
- ],
+ rules: RULES,
},
- plugins,
+ shortcodes: registry.getEditorComponents(),
};
}
shouldComponentUpdate(nextProps, nextState) {
- if (this.state.editorState.equals(nextState.editorState)) {
- return false
- }
- return true;
+ return !this.state.editorState.equals(nextState.editorState);
}
handlePaste = (e, data, state) => {
@@ -217,12 +39,12 @@ export default class Editor extends Component {
return;
}
const ast = htmlToSlate(data.html);
- const { document: doc } = SlateRaw.deserialize(ast, { terse: true });
+ const { document: doc } = Raw.deserialize(ast, { terse: true });
return state.transform().insertFragment(doc).apply();
}
handleDocumentChange = (doc, editorState) => {
- const raw = SlateRaw.serialize(editorState, { terse: true });
+ const raw = Raw.serialize(editorState, { terse: true });
const mdast = slateToRemark(raw);
this.props.onChange(mdast);
};
@@ -230,70 +52,6 @@ export default class Editor extends Component {
hasMark = type => this.state.editorState.marks.some(mark => mark.type === type);
hasBlock = type => this.state.editorState.blocks.some(node => node.type === type);
- handleKeyDown = (e, data, state) => {
- const createDefaultBlock = () => {
- return SlateBlock.create({
- type: 'paragraph',
- nodes: [SlateText.createFromString('')]
- });
- };
- if (data.key === 'enter') {
- /**
- * If "Enter" is pressed while a single void block is selected, a new
- * paragraph should be added above or below it, and the current selection
- * should be collapsed to the start of the new paragraph.
- *
- * If the selected block is the first block in the document, create the
- * new block above it. If not, create the new block below it.
- */
- const { document: doc, selection, anchorBlock, focusBlock } = state;
- const singleBlockSelected = anchorBlock === focusBlock;
- if (!singleBlockSelected || !focusBlock.isVoid) return;
-
- e.preventDefault();
-
- const focusBlockParent = doc.getParent(focusBlock.key);
- const focusBlockIndex = focusBlockParent.nodes.indexOf(focusBlock);
- const focusBlockIsFirstChild = focusBlockIndex === 0;
-
- const newBlock = createDefaultBlock();
- const newBlockIndex = focusBlockIsFirstChild ? 0 : focusBlockIndex + 1;
-
- return state.transform()
- .insertNodeByKey(focusBlockParent.key, newBlockIndex, newBlock)
- .collapseToStartOf(newBlock)
- .apply();
- }
-
- if (data.isMod) {
-
- if (data.key === 'y') {
- e.preventDefault();
- return state.transform().redo().focus().apply({ save: false });
- }
-
- if (data.key === 'z') {
- e.preventDefault();
- return state.transform()[data.isShift ? 'redo' : 'undo']().focus().apply({ save: false });
- }
-
- const marks = {
- b: 'bold',
- i: 'italic',
- u: 'underlined',
- s: 'strikethrough',
- '`': 'code',
- };
-
- const mark = marks[data.key];
-
- if (mark) {
- e.preventDefault();
- return state.transform().toggleMark(mark).apply();
- }
- }
- };
-
handleMarkClick = (event, type) => {
event.preventDefault();
const resolvedState = this.state.editorState.transform().focus().toggleMark(type).apply();
@@ -310,7 +68,7 @@ export default class Editor extends Component {
// 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);
+ const transformed = transform.setBlock(isActive ? 'paragraph' : type);
}
// Handle the extra wrapping required for list buttons.
@@ -318,16 +76,16 @@ export default class Editor extends Component {
const isSameListType = editorState.blocks.some(block => {
return !!doc.getClosest(block.key, parent => parent.type === type);
});
- const isInList = EditListPlugin.utils.isSelectionInList(editorState);
+ const isInList = EditListConfigured.utils.isSelectionInList(editorState);
if (isInList && isSameListType) {
- EditListPlugin.transforms.unwrapList(transform, type);
+ EditListConfigured.transforms.unwrapList(transform, type);
} else if (isInList) {
const currentListType = type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list';
- EditListPlugin.transforms.unwrapList(transform, currentListType);
- EditListPlugin.transforms.wrapInList(transform, type);
+ EditListConfigured.transforms.unwrapList(transform, currentListType);
+ EditListConfigured.transforms.wrapInList(transform, type);
} else {
- EditListPlugin.transforms.wrapInList(transform, type);
+ EditListConfigured.transforms.wrapInList(transform, type);
}
}
@@ -381,7 +139,7 @@ export default class Editor extends Component {
shortcodeValue: plugin.toBlock(shortcodeData.toJS()),
shortcodeData,
};
- const nodes = [SlateText.createFromString('')];
+ const nodes = [Text.createFromString('')];
const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes };
const resolvedState = editorState.transform().insertBlock(block).apply();
this.ref.onChange(resolvedState);
@@ -401,17 +159,15 @@ export default class Editor extends Component {
render() {
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
- const { plugins, selectionPosition, dragging } = this.state;
return (
-