diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.css b/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.css
index c849baac..2d91b9f7 100644
--- a/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.css
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.css
@@ -1,14 +1,26 @@
.Toolbar {
position: absolute;
z-index: 1000;
- display: none;
+ display: none;
margin: none;
padding: none;
+ box-shadow: 1px 1px 5px;
list-style: none;
}
.Button {
display: inline-block;
+
+ & button {
+ padding: 5px;
+ border: none;
+ border-right: 1px solid #eee;
+ background: #fff;
+ }
+}
+
+.Button:last-child button {
+ border-right: none;
}
.Visible {
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js b/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js
index 69bb4d9b..fcfc2b23 100644
--- a/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/Toolbar.js
@@ -1,9 +1,12 @@
import React, { Component, PropTypes } from 'react';
+import { Icon } from '../../../UI';
import styles from './Toolbar.css';
-function button(label, action) {
+function button(label, icon, action) {
return (
-
+
);
}
@@ -11,6 +14,8 @@ export default class Toolbar extends Component {
static propTypes = {
isOpen: PropTypes.bool,
selectionPosition: PropTypes.object,
+ onH1: PropTypes.func.isRequired,
+ onH2: PropTypes.func.isRequired,
onBold: PropTypes.func.isRequired,
onItalic: PropTypes.func.isRequired,
onLink: PropTypes.func.isRequired,
@@ -36,7 +41,7 @@ export default class Toolbar extends Component {
};
render() {
- const { isOpen, onBold, onItalic, onLink } = this.props;
+ const { isOpen, onH1, onH2, onBold, onItalic, onLink } = this.props;
const classNames = [styles.Toolbar];
if (isOpen) {
@@ -45,9 +50,11 @@ export default class Toolbar extends Component {
return (
- {button('Bold', onBold)}
- {button('Italic', onItalic)}
- {button('Link', onLink)}
+ {button('Header 1', 'h1', onH1)}
+ {button('Header 2', 'h2', onH2)}
+ {button('Bold', 'bold', onBold)}
+ {button('Italic', 'italic', onItalic)}
+ {button('Link', 'link', onLink)}
);
}
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js
index 7d83939b..bcc7c673 100644
--- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js
+++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js
@@ -1,5 +1,8 @@
import React, { PropTypes } from 'react';
import { fromJS } from 'immutable';
+import MarkupIt from 'markup-it';
+import markdownSyntax from 'markup-it/syntaxes/markdown';
+import htmlSyntax from 'markup-it/syntaxes/html';
import CaretPosition from 'textarea-caret-position';
import registry from '../../../../lib/registry';
import MediaProxy from '../../../../valueObjects/MediaProxy';
@@ -9,6 +12,9 @@ import styles from './index.css';
const HAS_LINE_BREAK = /\n/m;
+const markdown = new MarkupIt(markdownSyntax);
+const html = new MarkupIt(htmlSyntax);
+
function processUrl(url) {
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
return url;
@@ -23,6 +29,42 @@ function preventDefault(e) {
e.preventDefault();
}
+function cleanupPaste(paste) {
+ const content = html.toContent(paste);
+ return markdown.toText(content);
+}
+
+function getCleanPaste(e) {
+ const transfer = e.clipboardData;
+ return new Promise((resolve) => {
+ const isHTML = !!Array.from(transfer.types).find(type => type === 'text/html');
+
+ if (isHTML) {
+ const data = transfer.getData('text/html');
+ // Avoid trying to clean up full HTML documents with head/body/etc
+ if (!data.match(/^\s* {
+ resolve(cleanupPaste(div.innerHTML));
+ document.body.removeChild(div);
+ }, 50);
+ return null;
+ }
+ }
+
+ e.preventDefault();
+ return resolve(transfer.getData(transfer.types[0]));
+ });
+}
+
const buildtInPlugins = fromJS([{
label: 'Image',
id: 'image',
@@ -64,6 +106,7 @@ export default class RawEditor extends React.Component {
this.element.addEventListener('dragenter', preventDefault, false);
this.element.addEventListener('dragover', preventDefault, false);
this.element.addEventListener('drop', this.handleDrop, false);
+ this.element.addEventListener('paste', this.handlePaste, false);
}
componentDidUpdate() {
@@ -127,6 +170,31 @@ export default class RawEditor extends React.Component {
this.props.onChange(beforeSelection + chars + afterSelection);
}
+ toggleHeader(header) {
+ const { value } = this.props;
+ const selection = this.getSelection();
+ const newSelection = Object.assign({}, selection);
+ const lastNewline = value.lastIndexOf('\n', selection.start);
+ const currentMatch = value.substr(lastNewline + 1).match(/^(#+)\s/);
+ const beforeHeader = value.substr(0, lastNewline + 1);
+ let afterHeader;
+ let chars;
+ if (currentMatch) {
+ afterHeader = value.substr(lastNewline + 1 + currentMatch[0].length);
+ chars = currentMatch[1] === header ? '' : `${ header } `;
+ const diff = chars.length - currentMatch[0].length;
+ newSelection.start += diff;
+ newSelection.end += diff;
+ } else {
+ afterHeader = value.substr(lastNewline + 1);
+ chars = `${ header } `;
+ newSelection.start += header.length + 1;
+ newSelection.end += header.length + 1;
+ }
+ this.newSelection = newSelection;
+ this.props.onChange(beforeHeader + chars + afterHeader);
+ }
+
updateHeight() {
if (this.element.scrollHeight > this.element.clientHeight) {
this.element.style.height = `${ this.element.scrollHeight }px`;
@@ -206,6 +274,12 @@ export default class RawEditor extends React.Component {
this.setState({ showBlockMenu: false });
};
+ handleHeader(header) {
+ return () => {
+ this.toggleHeader(header);
+ };
+ }
+
handleDrop = (e) => {
e.preventDefault();
let data;
@@ -226,6 +300,20 @@ export default class RawEditor extends React.Component {
this.replaceSelection(data);
};
+ handlePaste = (e) => {
+ const { value, onChange } = this.props;
+ const selection = this.getSelection();
+ const beforeSelection = value.substr(0, selection.start);
+ const afterSelection = value.substr(selection.end);
+
+ getCleanPaste(e).then((paste) => {
+ const newSelection = Object.assign({}, selection);
+ newSelection.start = newSelection.end = beforeSelection.length + paste.length;
+ this.newSelection = newSelection;
+ onChange(beforeSelection + paste + afterSelection);
+ });
+ }
+
render() {
const { onAddMedia, onRemoveMedia, getMedia } = this.props;
const { showToolbar, showBlockMenu, plugins, selectionPosition } = this.state;
@@ -233,6 +321,8 @@ export default class RawEditor extends React.Component {