diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js
index 473402ce..a63c98cb 100644
--- a/src/components/Widgets/MarkdownControl.js
+++ b/src/components/Widgets/MarkdownControl.js
@@ -30,7 +30,7 @@ class MarkdownControl extends React.Component {
};
render() {
- const { editor, onChange, onAddMedia, getMedia, value } = this.props;
+ const { editor, onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props;
if (editor.get('useVisualMode')) {
return (
@@ -51,6 +51,7 @@ class MarkdownControl extends React.Component {
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css b/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css
new file mode 100644
index 00000000..5183a9e7
--- /dev/null
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css
@@ -0,0 +1,134 @@
+.root {
+ position: absolute;
+ left: -18px;
+ display: none;
+ width: 100%;
+}
+
+.visible {
+ display: block;
+}
+
+.button {
+ display: block;
+ width: 15px;
+ height: 15px;
+ border: 1px solid #444;
+ border-radius: 100%;
+ background: transparent;
+ line-height: 13px;
+}
+
+.expanded {
+ display: block;
+}
+
+.collapsed {
+ display: none;
+}
+
+.pluginForm,
+.menu {
+ margin-top: -20px;
+ margin-bottom: 30px;
+ margin-left: 20px;
+ border-radius: 4px;
+ background: #fff;
+ box-shadow: 1px 1px 20px;
+
+ & h3 {
+ padding: 5px 20px;
+ border-bottom: 1px solid;
+ }
+}
+
+.menu {
+ list-style: none;
+
+ & li button {
+ display: block;
+ padding: 5px 20px;
+ min-width: 30%;
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid;
+ background: #fff;
+ -webkit-appearance: none;
+ }
+
+ & li:last-child button {
+ border-bottom: none;
+ }
+
+ & li:hover button {
+ background: #efefef;
+ }
+}
+
+.menu.expanded {
+ display: inline-block;
+}
+
+.control {
+ position: relative;
+ padding: 20px 20px;
+ color: #7c8382;
+
+ & input,
+ & textarea,
+ & select {
+ display: block;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ outline: 0;
+ border: none;
+ background: 0 0;
+ box-shadow: none;
+ color: #7c8382;
+ font-size: 18px;
+ font-family: monospace;
+ }
+}
+
+.label {
+ display: block;
+ margin-bottom: 18px;
+ color: #aab0af;
+ font-size: 12px;
+}
+
+.widget {
+ position: relative;
+ border-bottom: 1px solid #aaa;
+
+ &:after {
+ position: absolute;
+ bottom: -7px;
+ left: 42px;
+ z-index: 1;
+ width: 12px;
+ height: 12px;
+ border-right: 1px solid #aaa;
+ border-bottom: 1px solid #aaa;
+ background-color: #f2f5f4;
+ content: '';
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:last-child:after {
+ display: none;
+ }
+}
+
+.footer {
+ padding: 10px 20px;
+ border-top: 1px solid #eee;
+ background: #fff;
+ text-align: right;
+}
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.js b/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.js
new file mode 100644
index 00000000..0e060a6d
--- /dev/null
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.js
@@ -0,0 +1,135 @@
+import React, { Component, PropTypes } from 'react';
+import { fromJS } from 'immutable';
+import { Button } from 'react-toolbox/lib/button';
+import { resolveWidget } from '../../../Widgets';
+import styles from './BlockMenu.css';
+
+export default class BlockMenu extends Component {
+ static propTypes = {
+ isOpen: PropTypes.bool,
+ selectionPosition: PropTypes.object,
+ plugins: PropTypes.object.isRequired,
+ onBlock: PropTypes.func.isRequired,
+ onAddMedia: PropTypes.func.isRequired,
+ onRemoveMedia: PropTypes.func.isRequired,
+ getMedia: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isExpanded: false,
+ openPlugin: null,
+ pluginData: fromJS({}),
+ };
+ }
+
+ componentDidUpdate() {
+ const { selectionPosition } = this.props;
+ if (selectionPosition) {
+ const style = this.element.style;
+ style.setProperty('top', `${ selectionPosition.top }px`);
+ }
+ }
+
+ handleToggle = (e) => {
+ e.preventDefault();
+ this.setState({ isExpanded: !this.state.isExpanded });
+ };
+
+ handleRef = (ref) => {
+ this.element = ref;
+ };
+
+ handlePlugin(plugin) {
+ return (e) => {
+ e.preventDefault();
+ this.setState({ openPlugin: plugin, pluginData: fromJS({}) });
+ };
+ }
+
+ buttonFor(plugin) {
+ return (
+
+ );
+ }
+
+ handleSubmit = (e) => {
+ e.preventDefault();
+ const { openPlugin, pluginData } = this.state;
+ const toBlock = openPlugin.get('toBlock');
+ this.props.onBlock(toBlock.call(toBlock, pluginData.toJS()));
+ this.setState({ openPlugin: null, isExpanded: false });
+ };
+
+ handleCancel = (e) => {
+ e.preventDefault();
+ this.setState({ openPlugin: null, isExpanded: false });
+ };
+
+ controlFor(field) {
+ const { onAddMedia, onRemoveMedia, getMedia } = this.props;
+ const { pluginData } = this.state;
+ const widget = resolveWidget(field.get('widget') || 'string');
+ const value = pluginData.get(field.get('name'));
+
+ return (
+
+
+ {
+ React.createElement(widget.control, {
+ field,
+ value,
+ onChange: (val) => {
+ this.setState({
+ pluginData: pluginData.set(field.get('name'), val),
+ });
+ },
+ onAddMedia,
+ onRemoveMedia,
+ getMedia,
+ })
+ }
+
+ );
+ }
+
+ pluginForm(plugin) {
+ return (
);
+ }
+
+ render() {
+ const { isOpen, plugins } = this.props;
+ const { isExpanded, openPlugin } = this.state;
+ const classNames = [styles.root];
+ if (isOpen) {
+ classNames.push(styles.visible);
+ }
+ if (openPlugin) {
+ classNames.push(styles.openPlugin);
+ }
+
+ return (
+
+
+ {plugins.map(plugin => this.buttonFor(plugin))}
+
+ {openPlugin && this.pluginForm(openPlugin)}
+
);
+ }
+}
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js b/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js
index 4f539501..69bb4d9b 100644
--- a/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js
@@ -10,7 +10,7 @@ function button(label, action) {
export default class Toolbar extends Component {
static propTypes = {
isOpen: PropTypes.bool,
- selectionPosition: PropTypes.node,
+ selectionPosition: PropTypes.object,
onBold: PropTypes.func.isRequired,
onItalic: PropTypes.func.isRequired,
onLink: PropTypes.func.isRequired,
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js
index aaac11ce..89aa9d20 100644
--- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js
@@ -1,7 +1,10 @@
import React, { PropTypes } from 'react';
+import { fromJS } from 'immutable';
import CaretPosition from 'textarea-caret-position';
-import Toolbar from './Toolbar';
+import registry from '../../../../lib/registry';
import MediaProxy from '../../../../valueObjects/MediaProxy';
+import Toolbar from './Toolbar';
+import BlockMenu from './BlockMenu';
import styles from './index.css';
const HAS_LINE_BREAK = /\n/m;
@@ -20,10 +23,36 @@ function preventDefault(e) {
e.preventDefault();
}
+const buildtInPlugins = fromJS([{
+ label: 'Image',
+ id: 'image',
+ fromBlock: (data) => {
+ const m = data.match(/^!\[([^\]]+)\]\(([^\)]+)\)$/);
+ return m && {
+ image: m[2],
+ alt: m[1],
+ };
+ },
+ toBlock: data => ``,
+ toPreview: data => `

`,
+ pattern: /^!\[([^\]]+)\]\(([^\)]+)\)$/,
+ fields: [{
+ label: 'Image',
+ name: 'image',
+ widget: 'image',
+ }, {
+ label: 'Alt Text',
+ name: 'alt',
+ }],
+}]);
+
export default class RawEditor extends React.Component {
constructor(props) {
super(props);
- this.state = {};
+ const plugins = registry.getEditorComponents();
+ this.state = {
+ plugins: buildtInPlugins.concat(plugins),
+ };
this.shortcuts = {
meta: {
b: this.handleBold,
@@ -111,10 +140,6 @@ export default class RawEditor extends React.Component {
}
};
- handleToolbarRef = (ref) => {
- this.toolbar = ref;
- };
-
handleKey = (e) => {
if (e.metaKey) {
const action = this.shortcuts.meta[e.key];
@@ -140,16 +165,34 @@ export default class RawEditor extends React.Component {
};
handleSelection = () => {
+ const { value } = this.props;
const selection = this.getSelection();
if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) {
try {
const position = this.caretPosition.get(selection.start, selection.end);
- this.setState({ showToolbar: true, selectionPosition: position });
+ this.setState({ showToolbar: true, showBlockMenu: false, selectionPosition: position });
} catch (e) {
- this.setState({ showToolbar: false });
+ this.setState({ showToolbar: false, showBlockMenu: false });
+ }
+ } else if (selection.start === selection.end) {
+ const newBlock =
+ (
+ selection.start === 0 ||
+ value.substr(selection.start - 2, 2) === '\n\n') &&
+ (
+ selection.end === (value.length - 1) ||
+ value.substr(selection.end, 2) === '\n\n' ||
+ value.substr(selection.end).match(/\n*$/m)
+ );
+
+ if (newBlock) {
+ const position = this.caretPosition.get(selection.start, selection.end);
+ this.setState({ showToolbar: false, showBlockMenu: true, selectionPosition: position });
+ } else {
+ this.setState({ showToolbar: false, showBlockMenu: false });
}
} else {
- this.setState({ showToolbar: false });
+ this.setState({ showToolbar: false, showBlockMenu: false });
}
};
@@ -158,6 +201,11 @@ export default class RawEditor extends React.Component {
this.updateHeight();
};
+ handleBlock = (chars) => {
+ this.replaceSelection(chars);
+ this.setState({ showBlockMenu: false });
+ };
+
handleDrop = (e) => {
e.preventDefault();
let data;
@@ -179,16 +227,25 @@ export default class RawEditor extends React.Component {
};
render() {
- const { showToolbar, selectionPosition } = this.state;
+ const { onAddMedia, onRemoveMedia, getMedia } = this.props;
+ const { showToolbar, showBlockMenu, plugins, selectionPosition } = this.state;
return (
+