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-cell': props => <td {...props.attributes}>{props.children}</td>,
'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 => {
const { attributes, node, state: editorState } = props;
const isSelected = editorState.selection.hasFocusIn(node);
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 {
border: 2px solid black;
padding: 8px;
margin: 2px 0;
margin: 16px 0;
cursor: pointer;
}

View File

@ -26,7 +26,7 @@ export default class Editor extends Component {
marks: MARK_COMPONENTS,
rules: RULES,
},
shortcodes: registry.getEditorComponents(),
shortcodePlugins: registry.getEditorComponents(),
};
}
@ -45,7 +45,8 @@ export default class Editor extends Component {
handleDocumentChange = (doc, editorState) => {
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);
};
@ -136,12 +137,11 @@ export default class Editor extends Component {
const { editorState } = this.state;
const data = {
shortcode: plugin.id,
shortcodeValue: plugin.toBlock(shortcodeData.toJS()),
shortcodeData,
};
const nodes = [Text.createFromString('')];
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.setState({ editorState: resolvedState });
};
@ -180,7 +180,7 @@ export default class Editor extends Component {
codeBlock: this.getButtonProps('code', { isBlock: true }),
}}
onToggleMode={this.handleToggle}
plugins={this.state.shortcodes}
plugins={this.state.shortcodePlugins}
onSubmit={this.handlePluginSubmit}
onAddAsset={onAddAsset}
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;

View File

@ -143,17 +143,18 @@ const remarkShortcodes = ({ plugins }) => {
if (!nodeMayContainShortcode(node)) return node;
/**
* Combine the text values of all children to a single string, then
* check that string for a shortcode pattern match.
* Combine the text values of all children to a single string, check the
* 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 matchIsValid = validateMatch(text, match);
/**
* If a matching shortcode plugin is found, return a new node with shortcode
* data included. Otherwise, return the original node.
* If a valid match is found, return a new node with shortcode data
* 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 };
}
/**
* 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
* 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 u('html', valueHtml);
const textNode = u('html', valueHtml);
const children = [ textNode ];
return { ...node, children };
}
};
@ -471,7 +481,7 @@ export const remarkToSlate = mdast => {
return result;
};
export const slateToRemark = raw => {
export const slateToRemark = (raw, shortcodePlugins) => {
const typeMap = {
'paragraph': 'paragraph',
'heading-one': 'heading',
@ -532,7 +542,9 @@ export const slateToRemark = raw => {
if (node.type === 'shortcode') {
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 ]);
}