fix slate migration bugs

This commit is contained in:
Shawn Erquhart 2017-11-16 16:37:02 -05:00
parent 1bbaebf6d5
commit 9342c9c064
6 changed files with 114 additions and 62 deletions

View File

@ -10,8 +10,7 @@
& input,
& textarea,
& select,
& div[contenteditable=true] {
& select {
@apply --input;
}
}

View File

@ -3,6 +3,7 @@
}
.nc-rawEditor-rawEditor {
@apply(--input);
position: relative;
overflow: hidden;
overflow-x: auto;

View File

@ -14,6 +14,7 @@
}
.nc-visualEditor-editor {
@apply(--input);
position: relative;
overflow: hidden;
overflow-x: auto;

View File

@ -8,65 +8,96 @@ import cn from 'classnames';
* by us when we manually deserialize from Remark's MDAST to Slate's AST.
*/
/**
* Mark Components
*/
const Bold = props => <strong>{props.children}</strong>;
const Italic = props => <em>{props.children}</em>;
const Strikethrough = props => <s>{props.children}</s>;
const Code = props => <code>{props.children}</code>;
/**
* Node Components
*/
const Paragraph = props => <p {...props.attributes}>{props.children}</p>;
const ListItem = props => <li {...props.attributes}>{props.children}</li>;
const Quote = props => <blockquote {...props.attributes}>{props.children}</blockquote>;
const CodeBlock = props => <pre><code {...props.attributes}>{props.children}</code></pre>;
const HeadingOne = props => <h1 {...props.attributes}>{props.children}</h1>;
const HeadingTwo = props => <h2 {...props.attributes}>{props.children}</h2>;
const HeadingThree = props => <h3 {...props.attributes}>{props.children}</h3>;
const HeadingFour = props => <h4 {...props.attributes}>{props.children}</h4>;
const HeadingFive = props => <h5 {...props.attributes}>{props.children}</h5>;
const HeadingSix = props => <h6 {...props.attributes}>{props.children}</h6>;
const Table = props => <table><tbody {...props.attributes}>{props.children}</tbody></table>;
const TableRow = props => <tr {...props.attributes}>{props.children}</tr>;
const TableCell = props => <td {...props.attributes}>{props.children}</td>;
const ThematicBreak = props => <hr {...props.attributes}/>;
const BulletedList = props => <ul {...props.attributes}>{props.children}</ul>;
const NumberedList = props => (
<ol {...props.attributes} start={props.node.data.get('start') || 1}>{props.children}</ol>
);
const Link = props => {
const data = props.node.get('data');
const marks = data.get('marks');
const url = data.get('url');
const title = data.get('title');
const link = <a href={url} title={title} {...props.attributes}>{props.children}</a>;
const result = !marks ? link : marks.reduce((acc, mark) => {
const MarkComponent = MARK_COMPONENTS[mark.type];
return <MarkComponent>{acc}</MarkComponent>;
}, link);
return result;
};
const Image = props => {
const data = props.node.get('data');
const marks = data.get('marks');
const url = data.get('url');
const title = data.get('title');
const alt = data.get('alt');
const image = <img src={url} title={title} alt={alt} {...props.attributes}/>;
const result = !marks ? image : marks.reduce((acc, mark) => {
const MarkComponent = MARK_COMPONENTS[mark.type];
return <MarkComponent>{acc}</MarkComponent>;
}, image);
return result;
};
const Shortcode = props => {
const { attributes, node, editor } = props;
const isSelected = editor.value.selection.hasFocusIn(node);
const className = cn('nc-visualEditor-shortcode', { ['nc-visualEditor-shortcodeSelected']: isSelected });
return <div {...attributes} className={className} draggable >{node.data.get('shortcode')}</div>;
};
export const renderMark = props => {
switch (props.mark.type) {
case bold: return props => <strong>{props.children}</strong>;
case italic: return props => <em>{props.children}</em>;
case strikethrough: return props => <s>{props.children}</s>;
case code: return props => <code>{props.children}</code>;
case 'bold': return <Bold {...props}/>;
case 'italic': return <Italic {...props}/>;
case 'strikethrough': return <Strikethrough {...props}/>;
case 'code': return <Code {...props}/>;
}
};
export const renderNode = props => {
switch (props.node.type) {
case 'paragraph': return props => <p {...props.attributes}>{props.children}</p>;
case 'list-item': return props => <li {...props.attributes}>{props.children}</li>;
case 'quote': return props => <blockquote {...props.attributes}>{props.children}</blockquote>;
case 'code': return props => <pre><code {...props.attributes}>{props.children}</code></pre>;
case 'heading-one': return props => <h1 {...props.attributes}>{props.children}</h1>;
case 'heading-two': return props => <h2 {...props.attributes}>{props.children}</h2>;
case 'heading-three': return props => <h3 {...props.attributes}>{props.children}</h3>;
case 'heading-four': return props => <h4 {...props.attributes}>{props.children}</h4>;
case 'heading-five': return props => <h5 {...props.attributes}>{props.children}</h5>;
case 'heading-six': return props => <h6 {...props.attributes}>{props.children}</h6>;
case 'table': return props => <table><tbody {...props.attributes}>{props.children}</tbody></table>;
case 'table-row': return props => <tr {...props.attributes}>{props.children}</tr>;
case 'table-cell': return props => <td {...props.attributes}>{props.children}</td>;
case 'thematic-break': return props => <hr {...props.attributes}/>;
case 'bulleted-list': return props => <ul {...props.attributes}>{props.children}</ul>;
case 'numbered-list': return props => (
<ol {...props.attributes} start={props.node.data.get('start') || 1}>{props.children}</ol>
);
case 'link': return props => {
const data = props.node.get('data');
const marks = data.get('marks');
const url = data.get('url');
const title = data.get('title');
const link = <a href={url} title={title} {...props.attributes}>{props.children}</a>;
const result = !marks ? link : marks.reduce((acc, mark) => {
const MarkComponent = MARK_COMPONENTS[mark.type];
return <MarkComponent>{acc}</MarkComponent>;
}, link);
return result;
};
case 'image': props => {
const data = props.node.get('data');
const marks = data.get('marks');
const url = data.get('url');
const title = data.get('title');
const alt = data.get('alt');
const image = <img src={url} title={title} alt={alt} {...props.attributes}/>;
const result = !marks ? image : marks.reduce((acc, mark) => {
const MarkComponent = MARK_COMPONENTS[mark.type];
return <MarkComponent>{acc}</MarkComponent>;
}, image);
return result;
};
case 'shortcode': props => {
const { attributes, node, editor } = props;
const isSelected = editor.value.selection.hasFocusIn(node);
const className = cn('nc-visualEditor-shortcode', { ['nc-visualEditor-shortcodeSelected']: isSelected });
return <div {...attributes} className={className} draggable >{node.data.get('shortcode')}</div>;
};
case 'paragraph': return <Paragraph {...props}/>;
case 'list-item': return <ListItem {...props}/>;
case 'quote': return <Quote {...props}/>;
case 'code': return <CodeBlock {...props}/>;
case 'heading-one': return <HeadingOne {...props}/>;
case 'heading-two': return <HeadingTwo {...props}/>;
case 'heading-three': return <HeadingThree {...props}/>;
case 'heading-four': return <HeadingFour {...props}/>;
case 'heading-five': return <HeadingFive {...props}/>;
case 'heading-six': return <HeadingSix {...props}/>;
case 'table': return <Table {...props}/>;
case 'table-row': return <TableRow {...props}/>;
case 'table-cell': return <TableCell {...props}/>;
case 'thematic-break': return <ThematicBreak {...props}/>;
case 'bulleted-list': return <BulletedList {...props}/>;
case 'numbered-list': return <NumberedList {...props}/>;
case 'link': return <Link {...props}/>;
case 'image': return <Image {...props}/>;
case 'shortcode': return <Shortcode {...props}/>;
}
};

View File

@ -9,6 +9,7 @@ export function validateNode(node) {
* Validation of the document itself.
*/
if (node.kind === 'document') {
const doc = node;
/**
* If the editor is ever in an empty state, insert an empty
* paragraph block.
@ -28,26 +29,45 @@ export function validateNode(node) {
/**
* Ensure that shortcodes are children of the root node.
*/
const nestedShortcode = node.findDescendant(descendant => {
const nestedShortcode = doc.findDescendant(descendant => {
const { type, key } = descendant;
return type === 'shortcode' && node.getParent(key).key !== node.key;
return type === 'shortcode' && doc.getParent(key).key !== doc.key;
});
if (nestedShortcode) {
return change => change.unwrapNodeByKey(node.key);
const unwrapShortcode = change => {
const key = nestedShortcode.key;
const newDoc = change.value.document;
const newParent = newDoc.getParent(key);
const docIsParent = newParent.key === newDoc.key;
const newParentParent = newDoc.getParent(newParent.key);
const docIsParentParent = newParentParent && newParentParent.key === newDoc.key;
if (docIsParent) {
return change;
}
/**
* Normalization happens by default, and causes all validation to
* restart with the result of a change upon execution. This unwrap loop
* could temporarily place a shortcode node in conflict with an outside
* plugin's schema, resulting in an infinite loop. To ensure against
* this, we turn off normalization until the last change.
*/
change.unwrapNodeByKey(nestedShortcode.key, { normalize: docIsParentParent });
};
return unwrapShortcode;
}
/**
* Ensure that trailing shortcodes are followed by an empty paragraph.
*/
const trailingShortcode = node.findDescendant(descendant => {
const trailingShortcode = doc.findDescendant(descendant => {
const { type, key } = descendant;
return type === 'shortcode' && node.getBlocks().last().key === key;
return type === 'shortcode' && doc.getBlocks().last().key === key;
});
if (trailingShortcode) {
return change => {
const text = Text.create('');
const block = Block.create({ type: 'paragraph', nodes: [ text ] });
return change.insertNodeByKey(node.key, node.get('nodes').size, block);
return change.insertNodeByKey(doc.key, doc.get('nodes').size, block);
};
}
}

View File

@ -249,7 +249,7 @@ function convertTextNode(node) {
/**
* Process Slate node leaves in preparation for MDAST transformation.
*/
function processLeaves(leaves) {
function processLeaves(leaf) {
/**
* Get an array of the mark types, converted to their MDAST equivalent
* types.