diff --git a/example/index.html b/example/index.html index 2d910b39..4389ed9d 100644 --- a/example/index.html +++ b/example/index.html @@ -68,5 +68,22 @@ + diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js index 946b486e..9fb9d149 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js @@ -17,9 +17,11 @@ export default class BlockTypesMenu extends Component { this.toggleMenu = this.toggleMenu.bind(this); this.handleOpen = this.handleOpen.bind(this); this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); + this.handlePluginClick = this.handlePluginClick.bind(this); this.handleFileUploadClick = this.handleFileUploadClick.bind(this); this.handleFileUploadChange = this.handleFileUploadChange.bind(this); this.renderBlockTypeButton = this.renderBlockTypeButton.bind(this); + this.renderPluginButton = this.renderPluginButton.bind(this); } /** @@ -58,6 +60,14 @@ export default class BlockTypesMenu extends Component { this.props.onClickBlock(type); } + handlePluginClick(e, plugin) { + const data = {}; + plugin.fields.forEach(field => { + data[field] = window.prompt(field); + }); + this.props.onClickPlugin(plugin.id, data); + } + handleFileUploadClick() { this._fileInput.click(); } @@ -84,7 +94,6 @@ export default class BlockTypesMenu extends Component { } - renderBlockTypeButton(type, icon) { const onClick = e => this.handleBlockTypeClick(e, type); return ( @@ -92,13 +101,20 @@ export default class BlockTypesMenu extends Component { ); } + renderPluginButton(plugin) { + const onClick = e => this.handlePluginClick(e, plugin); + return ( + + ); + } + renderMenu() { const { plugins } = this.props; if (this.state.expanded) { return (
{this.renderBlockTypeButton('hr', 'dot-3')} - {plugins.map(plugin => this.renderBlockTypeButton(plugin.id, plugin.icon))} + {plugins.map(plugin => this.renderPluginButton(plugin))} plugin.id)); } else { rawJson = emptyParagraphBlock; } @@ -53,6 +56,7 @@ class VisualEditor extends React.Component { this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this); this.handleInlineClick = this.handleInlineClick.bind(this); this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); + this.handlePluginClick = this.handlePluginClick.bind(this); this.handleImageClick = this.handleImageClick.bind(this); this.focusAndAddParagraph = this.focusAndAddParagraph.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); @@ -65,19 +69,6 @@ class VisualEditor extends React.Component { return this.props.getMedia(src); } - /** - * Custom local renderer for image proxy. - */ - customImageNodeRenderer(editorProps) { - const { node, state } = editorProps; - const isFocused = state.selection.hasEdgeIn(node); - const className = isFocused ? styles.active : null; - const src = node.data.get('src'); - return ( - - ); - } - /** * Slate keeps track of selections, scroll position etc. * So, onChange gets dispatched on every interaction (click, arrows, everything...) @@ -235,6 +226,24 @@ class VisualEditor extends React.Component { this.setState({ state }, this.focusAndAddParagraph); } + handlePluginClick(type, data) { + let { state } = this.state; + + state = state + .transform() + .insertInline({ + type: type, + data: data, + isVoid: true + }) + .collapseToEnd() + .insertBlock(DEFAULT_NODE) + .focus() + .apply(); + + this.setState({ state }); + } + handleImageClick(mediaProxy) { let { state } = this.state; this.props.onAddMedia(mediaProxy); @@ -294,6 +303,7 @@ class VisualEditor extends React.Component { plugins={getPlugins()} position={this.menuPositions.blockTypesMenu} onClickBlock={this.handleBlockTypeClick} + onClickPlugin={this.handlePluginClick} onClickImage={this.handleImageClick} /> ); diff --git a/src/components/Widgets/MarkdownControlElements/syntax.js b/src/components/Widgets/MarkdownControlElements/syntax.js deleted file mode 100644 index 6a9a6984..00000000 --- a/src/components/Widgets/MarkdownControlElements/syntax.js +++ /dev/null @@ -1,77 +0,0 @@ -import Immutable from 'immutable'; -import MarkupIt from 'markup-it'; -import markdownSyntax from 'markup-it/syntaxes/markdown'; -import reInline from 'markup-it/syntaxes/markdown/re/inline'; - - -/** - * Test if a link input is an image - * @param {String} raw - * @return {Boolean} - */ -function isImage(raw) { - return raw.charAt(0) === '!'; -} - -export default function getSyntax(getMedia) { - const customImageRule = MarkupIt.Rule('mediaproxy') - .regExp(reInline.link, function(state, match) { - if (!isImage(match[0])) { - return; - } - - var imgData = Immutable.Map({ - alt: match[1], - src: getMedia(match[2]), - title: match[3] - }).filter(Boolean); - - return { - data: imgData - }; - }) - .regExp(reInline.reflink, function(state, match) { - if (!isImage(match[0])) { - return; - } - - var refId = (match[2] || match[1]); - return { - data: { ref: refId } - }; - }) - .regExp(reInline.nolink, function(state, match) { - if (!isImage(match[0])) { - return; - } - - var refId = (match[2] || match[1]); - return { - data: { ref: refId } - }; - }) - .regExp(reInline.reffn, function(state, match) { - if (!isImage(match[0])) { - return; - } - - var refId = match[1]; - return { - data: { ref: refId } - }; - }) - .toText(function(state, token) { - var data = token.getData(); - var alt = data.get('alt', ''); - var src = getMedia(data.get('src', '')); - var title = data.get('title', ''); - - if (title) { - return '![' + alt + '](' + src + ' "' + title + '")'; - } else { - return '![' + alt + '](' + src + ')'; - } - }); - - return markdownSyntax.addInlineRules(customImageRule); -} diff --git a/src/components/Widgets/richText.js b/src/components/Widgets/richText.js index bb11631d..0b8b6856 100644 --- a/src/components/Widgets/richText.js +++ b/src/components/Widgets/richText.js @@ -1,8 +1,10 @@ +/* eslint react/prop-types: 0, react/no-multi-comp: 0 */ import React from 'react'; -import { List } from 'immutable'; +import { List, Map } from 'immutable'; import MarkupIt from 'markup-it'; import markdownSyntax from 'markup-it/syntaxes/markdown'; import htmlSyntax from 'markup-it/syntaxes/html'; +import reInline from 'markup-it/syntaxes/markdown/re/inline'; import { Icon } from '../UI'; /* @@ -15,8 +17,8 @@ let processedPlugins = List([]); const nodes = {}; -const augmentedMarkdownSyntax = markdownSyntax; -const augmentedHTMLSyntax = htmlSyntax; +let augmentedMarkdownSyntax = markdownSyntax; +let augmentedHTMLSyntax = htmlSyntax; function processEditorPlugins(plugins) { // Since the plugin list is immutable, a simple comparisson is enough @@ -25,36 +27,86 @@ function processEditorPlugins(plugins) { plugins.forEach(plugin => { const markdownRule = MarkupIt.Rule(plugin.id) - .regExp(plugin.pattern, function(state, match) { return plugin.fromBlock(match); }) - .toText(function(state, token) { return plugin.toBlock(token.getData()); }); + .regExp(plugin.pattern, function(state, match) { + return { data: plugin.fromBlock(match) }; + }) + .toText(function(state, token) { return plugin.toBlock(token.getData().toObject()) + '\n\n'; }); const htmlRule = MarkupIt.Rule(plugin.id) .regExp(plugin.pattern, function(state, match) { return plugin.fromBlock(match); }) .toText(function(state, token) { return plugin.toPreview(token.getData()); }); const nodeRenderer = (props) => { - /* eslint react/prop-types: 0 */ const { node, state } = props; const isFocused = state.selection.hasEdgeIn(node); const className = isFocused ? 'plugin active' : 'plugin'; return (
- +
+
+ { plugin.fields.map(field => `${field}: “${node.data.get(field)}”`) } +
+ +
); }; - augmentedMarkdownSyntax.addInlineRules(markdownRule); - augmentedHTMLSyntax.addInlineRules(htmlRule); + augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(markdownRule); + augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(htmlRule); nodes[plugin.id] = nodeRenderer; }); processedPlugins = plugins; } +function processMediaProxyPlugins(getMedia) { + const customImageRule = MarkupIt.Rule('mediaproxy') + .regExp(reInline.link, function(state, match) { + if (match[0].charAt(0) !== '!') { + // Return if this is not an image + return; + } + + var imgData = Map({ + alt: match[1], + src: getMedia(match[2]), + title: match[3] + }).filter(Boolean); + + return { + data: imgData + }; + }) + .toText(function(state, token) { + var data = token.getData(); + var alt = data.get('alt', ''); + var src = getMedia(data.get('src', '')); + var title = data.get('title', ''); + + if (title) { + return '![' + alt + '](' + src + ' "' + title + '")'; + } else { + return '![' + alt + '](' + src + ')'; + } + }); + + nodes['mediaproxy'] = (props) => { + /* eslint react/prop-types: 0 */ + const { node, state } = props; + const isFocused = state.selection.hasEdgeIn(node); + const className = isFocused ? 'active' : null; + const src = node.data.get('src'); + return ( + + ); + }; + augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(customImageRule); +} + function getPlugins() { return processedPlugins.map(plugin => ( - { id: plugin.id, icon: plugin.icon } + { id: plugin.id, icon: plugin.icon, fields: plugin.fields } )).toArray(); } @@ -62,7 +114,8 @@ function getNodes() { return nodes; } -function getSyntaxes() { +function getSyntaxes(getMedia) { + processMediaProxyPlugins(getMedia); return { markdown: augmentedMarkdownSyntax, html:augmentedHTMLSyntax }; }