diff --git a/package.json b/package.json
index 5ba48323..3ba523ce 100644
--- a/package.json
+++ b/package.json
@@ -162,7 +162,7 @@
"remark-stringify": "^3.0.1",
"selection-position": "^1.0.0",
"semaphore": "^1.0.5",
- "slate": "^0.20.3",
+ "slate": "^0.20.6",
"slate-drop-or-paste-images": "^0.2.0",
"slate-edit-list": "^0.7.1",
"slug": "^0.9.1",
diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css
index 9a1ec80b..81ab24db 100644
--- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css
+++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.css
@@ -100,3 +100,15 @@
margin-left: 0; margin-right: 0;
}
}
+
+.shortcode {
+ border: 2px solid black;
+ padding: 8px;
+ margin: 2px 0;
+ cursor: pointer;
+}
+
+.shortcodeSelected {
+ border-color: var(--primaryColor);
+ color: var(--primaryColor);
+}
diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js
index af58117a..f0871fd3 100644
--- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js
+++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js
@@ -1,6 +1,9 @@
import React, { Component, PropTypes } from 'react';
-import { Map, List } from 'immutable';
-import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw} from 'slate';
+import ReactDOMServer from 'react-dom/server';
+import { Map, List, fromJS } from 'immutable';
+import { reduce, mapValues } from 'lodash';
+import cn from 'classnames';
+import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate';
import EditList from 'slate-edit-list';
import { markdownToHtml, htmlToMarkdown } from '../../unified';
import registry from '../../../../../lib/registry';
@@ -96,7 +99,8 @@ const MARK_TAGS = {
}
const BLOCK_COMPONENTS = {
- 'paragraph': props =>
{props.children}
,
+ 'container': props => {props.children}
,
+ 'paragraph': props => {props.children}
,
'list-item': props => {props.children},
'bulleted-list': props => ,
'numbered-list': props => {props.children}
,
@@ -110,11 +114,20 @@ const BLOCK_COMPONENTS = {
'heading-six': props => {props.children}
,
'image': props => {
const data = props.node && props.node.get('data');
- const src = data && data.get('src', props.src);
- const alt = data && data.get('alt', props.alt);
+ const src = data && data.get('src') || props.src;
+ const alt = data && data.get('alt') || props.alt;
return ;
},
};
+const getShortcodeId = props => {
+ if (props.node) {
+ const result = props.node.getIn(['data', 'shortcode', 'shortcodeId']);
+ return result || props.node.getIn(['data', 'shortcode']).shortcodeId;
+ }
+ return null;
+}
+
+const shortcodeStyles = {border: '2px solid black', padding: '8px', margin: '2px 0', cursor: 'pointer'};
const NODE_COMPONENTS = {
...BLOCK_COMPONENTS,
@@ -122,6 +135,19 @@ const NODE_COMPONENTS = {
const href = props.node && props.node.getIn(['data', 'href']) || props.href;
return {props.children};
},
+ 'shortcode': props => {
+ const { attributes, node, state: editorState } = props;
+ const isSelected = editorState.selection.hasFocusIn(node);
+ return (
+
+ {getShortcodeId(props)}
+
+ );
+ },
};
const MARK_COMPONENTS = {
@@ -133,6 +159,50 @@ const MARK_COMPONENTS = {
};
const RULES = [
+ {
+ deserialize(el, next) {
+ const shortcodeId = el.attribs && el.attribs['data-ncp'];
+ if (!shortcodeId) {
+ return;
+ }
+ const plugin = registry.getEditorComponents().get(shortcodeId);
+ if (!plugin) {
+ return;
+ }
+ const shortcodeData = Map(el.attribs).reduce((acc, value, key) => {
+ if (key.startsWith('data-ncp-')) {
+ const dataKey = key.slice('data-ncp-'.length).toLowerCase();
+ if (dataKey) {
+ return acc.set(dataKey, value);
+ }
+ }
+ return acc;
+ }, Map({ shortcodeId }));
+
+ const result = {
+ kind: 'block',
+ isVoid: true,
+ type: 'shortcode',
+ data: { shortcode: shortcodeData },
+ };
+ return result;
+ },
+ serialize(entity, children) {
+ if (entity.type !== 'shortcode') {
+ return;
+ }
+
+ const data = Map(entity.data.get('shortcode'));
+ const shortcodeId = data.get('shortcodeId');
+ const plugin = registry.getEditorComponents().get(shortcodeId);
+ const dataAttrs = data.delete('shortcodeId').mapKeys(key => `data-ncp-${key}`).set('data-ncp', shortcodeId);
+ const preview = plugin.toPreview(data.toJS());
+ const component = typeof preview === 'string'
+ ?
+ : {preview}
;
+ return component;
+ },
+ },
{
deserialize(el, next) {
const block = BLOCK_TAGS[el.tagName]
@@ -216,9 +286,9 @@ const RULES = [
const props = {
src: data.get('src'),
alt: data.get('alt'),
- attributes: data.get('attributes'),
};
- return NODE_COMPONENTS.image(props);
+ const result = NODE_COMPONENTS.image(props);
+ return result;
}
},
{
@@ -313,11 +383,44 @@ export default class Editor extends Component {
constructor(props) {
super(props);
const plugins = registry.getEditorComponents();
+ // Wrap value in div to ensure against trailing text outside of top level html element
+ const initialValue = this.props.value ? `${this.props.value}
` : '';
this.state = {
- editorState: serializer.deserialize(this.props.value || ''),
+ editorState: serializer.deserialize(initialValue),
schema: {
nodes: NODE_COMPONENTS,
marks: MARK_COMPONENTS,
+ rules: [
+ {
+ match: object => object.kind === 'document',
+ validate: doc => {
+ const blocks = doc.getBlocks();
+ const firstBlock = blocks.first();
+ const lastBlock = blocks.last();
+ const firstBlockIsVoid = firstBlock.isVoid;
+ const lastBlockIsVoid = lastBlock.isVoid;
+
+ if (firstBlockIsVoid || lastBlockIsVoid) {
+ return { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid };
+ }
+ },
+ normalize: (transform, doc, { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid }) => {
+ const block = SlateBlock.create({
+ type: 'paragraph',
+ nodes: [SlateText.createFromString('')],
+ });
+ if (firstBlockIsVoid) {
+ const { key } = transform.state.document;
+ transform.insertNodeByKey(key, 0, block);
+ }
+ if (lastBlockIsVoid) {
+ const { key, nodes } = transform.state.document;
+ transform.insertNodeByKey(key, nodes.size, block);
+ }
+ return transform;
+ },
+ }
+ ],
},
plugins,
};
@@ -425,57 +528,13 @@ export default class Editor extends Component {
};
handlePluginSubmit = (plugin, data) => {
- const { schema } = this.state;
- const nodeType = schema.nodes[`plugin_${ plugin.get('id') }`];
- //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(nodeType.create(data.toJS())).action());
- };
-
- handleDragEnter = (e) => {
- e.preventDefault();
- this.setState({ dragging: true });
- };
-
- handleDragLeave = (e) => {
- e.preventDefault();
- this.setState({ dragging: false });
- };
-
- handleDragOver = (e) => {
- e.preventDefault();
- };
-
- handleDrop = (e) => {
- e.preventDefault();
-
- this.setState({ dragging: false });
-
- const { schema } = this.state;
-
- const nodes = [];
-
- if (e.dataTransfer.files && e.dataTransfer.files.length) {
- Array.from(e.dataTransfer.files).forEach((file) => {
- createAssetProxy(file.name, file)
- .then((assetProxy) => {
- this.props.onAddAsset(assetProxy);
- if (file.type.split('/')[0] === 'image') {
- nodes.push(
- schema.nodes.image.create({ src: assetProxy.public_path, alt: file.name })
- );
- } else {
- nodes.push(
- schema.marks.link.create({ href: assetProxy.public_path, title: file.name })
- );
- }
- });
- });
- } else {
- nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain')));
- }
-
- nodes.forEach((node) => {
- //this.view.props.onAction(this.view.state.tr.replaceSelectionWith(node).action());
- });
+ const { editorState } = this.state;
+ const markdown = plugin.toBlock(data.toJS());
+ const html = markdownToHtml(markdown);
+ const block = serializer.deserialize(html).document.getBlocks().first();
+ const resolvedState = editorState.transform().insertBlock(block).apply();
+ this.ref.onChange(resolvedState);
+ this.setState({ editorState: resolvedState });
};
handleToggle = () => {
@@ -491,58 +550,49 @@ export default class Editor extends Component {
render() {
const { onAddAsset, onRemoveAsset, getAsset } = this.props;
const { plugins, selectionPosition, dragging } = this.state;
- const classNames = [styles.editor];
- if (dragging) {
- classNames.push(styles.dragging);
- }
- return (
-
-
+
+
+
+ this.setState({ editorState })}
+ onDocumentChange={this.handleDocumentChange}
+ onKeyDown={this.onKeyDown}
+ onPaste={this.handlePaste}
+ ref={ref => this.ref = ref}
+ spellCheck
/>
-
-
this.setState({ editorState })}
- onDocumentChange={this.handleDocumentChange}
- onKeyDown={this.onKeyDown}
- onPaste={this.handlePaste}
- ref={ref => this.ref = ref}
- spellCheck
- />
-
- );
+
+ );
}
}
diff --git a/src/components/Widgets/Markdown/unified.js b/src/components/Widgets/Markdown/unified.js
index 046ee2c5..ba011474 100644
--- a/src/components/Widgets/Markdown/unified.js
+++ b/src/components/Widgets/Markdown/unified.js
@@ -103,6 +103,9 @@ const rehypeShortcodes = () => {
const plugins = registry.getEditorComponents();
const transform = node => {
const { properties } = node;
+
+ // Convert this logic into a parseShortcodeDataFromHtml shared function, as
+ // this is also used in the visual editor serializer
const dataPrefix = `data${capitalize(shortcodeAttributePrefix)}`;
const pluginId = properties && properties[dataPrefix];
const plugin = plugins.get(pluginId);
@@ -131,6 +134,15 @@ const rehypeShortcodes = () => {
return transform;
}
+/**
+ * we can't escape the less than symbol
+ * which means how do we know {{}} from ?
+ * maybe we escape nothing
+ * then we can check for shortcodes in a unified plugin
+ * and only check against text nodes
+ * and maybe narrow the target text nodes even further somehow
+ * and make shortcode parsing faster
+ */
function remarkPrecompileShortcodes() {
const Compiler = this.Compiler;
const visitors = Compiler.prototype.visitors;