improve shortcode handling in visual editor

This commit is contained in:
Shawn Erquhart 2017-07-30 11:09:35 -04:00
parent ca60a6b8c9
commit 1d654662d2
5 changed files with 44 additions and 24 deletions

View File

@ -33,17 +33,10 @@ export const NODE_COMPONENTS = {
'table-row': props => <tr {...props.attributes}>{props.children}</tr>, 'table-row': props => <tr {...props.attributes}>{props.children}</tr>,
'table-cell': props => <td {...props.attributes}>{props.children}</td>, 'table-cell': props => <td {...props.attributes}>{props.children}</td>,
'thematic-break': props => <hr {...props.attributes}/>, 'thematic-break': props => <hr {...props.attributes}/>,
'shortcode-wrapper': props => <div {...props.attributes}>{props.children}</div>,
link: props => {
const data = props.node.get('data');
const url = data.get('url');
const title = data.get('title');
return <a href={href} title={title} {...props.attributes}>{props.children}</a>;
},
shortcode: props => { shortcode: props => {
const { attributes, node, state: editorState } = props; const { attributes, node, state: editorState } = props;
const isSelected = editorState.selection.hasFocusIn(node); const isSelected = editorState.selection.hasFocusIn(node);
const className = cn(styles.shortcode, { [styles.shortcodeSelected]: isSelected }); const className = cn(styles.shortcode, { [styles.shortcodeSelected]: isSelected });
return <span {...attributes} className={className} draggable >{node.data.get('shortcode')}</span>; return <div {...attributes} className={className} draggable >{node.data.get('shortcode')}</div>;
}, },
}; };

View File

@ -96,7 +96,7 @@
.shortcode { .shortcode {
border: 2px solid black; border: 2px solid black;
padding: 8px; padding: 8px;
margin: 2px 0; margin: 16px 0;
cursor: pointer; cursor: pointer;
} }

View File

@ -26,7 +26,7 @@ export default class Editor extends Component {
marks: MARK_COMPONENTS, marks: MARK_COMPONENTS,
rules: RULES, rules: RULES,
}, },
shortcodes: registry.getEditorComponents(), shortcodePlugins: registry.getEditorComponents(),
}; };
} }
@ -45,7 +45,8 @@ export default class Editor extends Component {
handleDocumentChange = (doc, editorState) => { handleDocumentChange = (doc, editorState) => {
const raw = Raw.serialize(editorState, { terse: true }); const raw = Raw.serialize(editorState, { terse: true });
const mdast = slateToRemark(raw); const plugins = this.state.shortcodePlugins;
const mdast = slateToRemark(raw, plugins);
this.props.onChange(mdast); this.props.onChange(mdast);
}; };
@ -136,12 +137,11 @@ export default class Editor extends Component {
const { editorState } = this.state; const { editorState } = this.state;
const data = { const data = {
shortcode: plugin.id, shortcode: plugin.id,
shortcodeValue: plugin.toBlock(shortcodeData.toJS()),
shortcodeData, shortcodeData,
}; };
const nodes = [Text.createFromString('')]; const nodes = [Text.createFromString('')];
const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes }; const block = { kind: 'block', type: 'shortcode', data, isVoid: true, nodes };
const resolvedState = editorState.transform().insertBlock(block).apply(); const resolvedState = editorState.transform().insertBlock(block).focus().apply();
this.ref.onChange(resolvedState); this.ref.onChange(resolvedState);
this.setState({ editorState: resolvedState }); this.setState({ editorState: resolvedState });
}; };
@ -180,7 +180,7 @@ export default class Editor extends Component {
codeBlock: this.getButtonProps('code', { isBlock: true }), codeBlock: this.getButtonProps('code', { isBlock: true }),
}} }}
onToggleMode={this.handleToggle} onToggleMode={this.handleToggle}
plugins={this.state.shortcodes} plugins={this.state.shortcodePlugins}
onSubmit={this.handlePluginSubmit} onSubmit={this.handlePluginSubmit}
onAddAsset={onAddAsset} onAddAsset={onAddAsset}
onRemoveAsset={onRemoveAsset} onRemoveAsset={onRemoveAsset}

View File

@ -25,6 +25,21 @@ const enforceNeverEmpty = {
}, },
}; };
const rules = [ enforceNeverEmpty ]; /**
* Ensure that shortcodes are children of the root node.
*/
const shortcodesAtRoot = {
match: object => object.kind === 'document',
validate: doc => {
return doc.findDescendant(node => {
return node.type === 'shortcode' && doc.getParent(node.key).key !== doc.key;
});
},
normalize: (transform, doc, node) => {
return transform.unwrapNodeByKey(node.key);
},
};
const rules = [ enforceNeverEmpty, shortcodesAtRoot ];
export default rules; export default rules;

View File

@ -143,17 +143,18 @@ const remarkShortcodes = ({ plugins }) => {
if (!nodeMayContainShortcode(node)) return node; if (!nodeMayContainShortcode(node)) return node;
/** /**
* Combine the text values of all children to a single string, then * Combine the text values of all children to a single string, check the
* check that string for a shortcode pattern match. * string for a shortcode pattern match, and validate the match.
*/ */
const text = mdastToString(node); const text = mdastToString(node).trim();
const { plugin, match } = matchTextToPlugin(text); const { plugin, match } = matchTextToPlugin(text);
const matchIsValid = validateMatch(text, match);
/** /**
* If a matching shortcode plugin is found, return a new node with shortcode * If a valid match is found, return a new node with shortcode data
* data included. Otherwise, return the original node. * included. Otherwise, return the original node.
*/ */
return plugin ? createShortcodeNode(text, plugin, match) : node; return matchIsValid ? createShortcodeNode(text, plugin, match) : node;
}; };
/** /**
@ -185,6 +186,13 @@ const remarkShortcodes = ({ plugins }) => {
return { plugin, match }; return { plugin, match };
} }
/**
* A match is only valid if it takes up the entire paragraph.
*/
function validateMatch(text, match) {
return match && match[0].length === text.length;
}
/** /**
* Create a new node with shortcode data included. Use an 'html' node instead * Create a new node with shortcode data included. Use an 'html' node instead
* of a 'text' node as the child to ensure the node content is not parsed by * of a 'text' node as the child to ensure the node content is not parsed by
@ -242,7 +250,9 @@ const remarkToRehypeShortcodes = ({ plugins, getAsset }) => {
/** /**
* Return a new 'html' type node containing the shortcode preview markup. * Return a new 'html' type node containing the shortcode preview markup.
*/ */
return u('html', valueHtml); const textNode = u('html', valueHtml);
const children = [ textNode ];
return { ...node, children };
} }
}; };
@ -471,7 +481,7 @@ export const remarkToSlate = mdast => {
return result; return result;
}; };
export const slateToRemark = raw => { export const slateToRemark = (raw, shortcodePlugins) => {
const typeMap = { const typeMap = {
'paragraph': 'paragraph', 'paragraph': 'paragraph',
'heading-one': 'heading', 'heading-one': 'heading',
@ -532,7 +542,9 @@ export const slateToRemark = raw => {
if (node.type === 'shortcode') { if (node.type === 'shortcode') {
const { data } = node; const { data } = node;
const textNode = u('html', data.shortcodeValue); const plugin = shortcodePlugins.get(data.shortcode);
const text = plugin.toBlock(data.shortcodeData);
const textNode = u('html', text);
return u('paragraph', { data }, [ textNode ]); return u('paragraph', { data }, [ textNode ]);
} }